diff --git a/registry/coder-labs/modules/gemini/README.md b/registry/coder-labs/modules/gemini/README.md index 65b9d9bc6..37fdca120 100644 --- a/registry/coder-labs/modules/gemini/README.md +++ b/registry/coder-labs/modules/gemini/README.md @@ -1,36 +1,41 @@ --- display_name: Gemini CLI +description: Run Gemini CLI in your workspace for AI pair programming icon: ../../../../.icons/gemini.svg -description: Run Gemini CLI in your workspace with AgentAPI integration verified: true tags: [agent, gemini, ai, google, tasks] --- # Gemini CLI -Run [Gemini CLI](https://ai.google.dev/gemini-api/docs/cli) in your workspace to access Google's Gemini AI models, and custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility. +Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace to access Google's Gemini AI models for interactive coding assistance and automated task execution. ```tf module "gemini" { - source = "registry.coder.com/coder-labs/gemini/coder" - version = "1.0.0" - agent_id = coder_agent.example.id - gemini_api_key = var.gemini_api_key - gemini_model = "gemini-2.5-pro" - install_gemini = true - gemini_version = "latest" - agentapi_version = "latest" + source = "registry.coder.com/coder-labs/gemini/coder" + version = "1.1.0" + agent_id = coder_agent.example.id + folder = "/home/coder/project" } ``` +## Features + +- **Interactive AI Assistance**: Run Gemini CLI directly in your terminal for coding help +- **Automated Task Execution**: Execute coding tasks automatically via AgentAPI integration +- **Multiple AI Models**: Support for Gemini 2.5 Pro, Flash, and other Google AI models +- **API Key Integration**: Seamless authentication with Gemini API +- **MCP Server Integration**: Built-in Coder MCP server for task reporting +- **Persistent Sessions**: Maintain context across workspace sessions + ## Prerequisites -- You must add the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module to your template - Node.js and npm will be installed automatically if not present +- The [Coder Login](https://registry.coder.com/modules/coder/coder-login) module is required -## Usage Example +## Examples -- Example 1: +### Basic setup ```tf variable "gemini_api_key" { @@ -40,39 +45,97 @@ variable "gemini_api_key" { } module "gemini" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder-labs/gemini/coder" - version = "1.0.0" - agent_id = coder_agent.example.id - gemini_api_key = var.gemini_api_key # we recommend providing this parameter inorder to have a smoother experience (i.e. no google sign-in) - gemini_model = "gemini-2.5-flash" - install_gemini = true - gemini_version = "latest" - gemini_instruction_prompt = "Start every response with `Gemini says:`" + source = "registry.coder.com/coder-labs/gemini/coder" + version = "1.1.0" + agent_id = coder_agent.example.id + gemini_api_key = var.gemini_api_key + folder = "/home/coder/project" } ``` -## How it Works +This basic setup will: + +- Install Gemini CLI in the workspace +- Configure authentication with your API key +- Set Gemini to run in `/home/coder/project` directory +- Enable interactive use from the terminal +- Set up MCP server integration for task reporting + +### Automated task execution (Experimental) + +> This functionality is in early access and is still evolving. +> For now, we recommend testing it in a demo or staging environment, +> rather than deploying to production +> +> Learn more in [the Coder documentation](https://coder.com/docs/ai-coder) + +```tf +variable "gemini_api_key" { + type = string + description = "Gemini API key" + sensitive = true +} + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/coder-login/coder" + version = "~> 1.0" + agent_id = coder_agent.example.id +} -- **Install**: The module installs Gemini CLI using npm (installs Node.js via NVM if needed) -- **Instruction Prompt**: If `GEMINI_INSTRUCTION_PROMPT` and `GEMINI_START_DIRECTORY` are set, creates the directory (if needed) and writes the prompt to `GEMINI.md` -- **Start**: Launches Gemini CLI in the specified directory, wrapped by AgentAPI -- **Environment**: Sets `GEMINI_API_KEY`, `GOOGLE_GENAI_USE_VERTEXAI`, `GEMINI_MODEL` for the CLI (if variables provided) +data "coder_parameter" "ai_prompt" { + type = "string" + name = "AI Prompt" + default = "" + description = "Task prompt for automated Gemini execution" + mutable = true +} + +module "gemini" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder-labs/gemini/coder" + version = "1.1.0" + agent_id = coder_agent.example.id + gemini_api_key = var.gemini_api_key + gemini_model = "gemini-2.5-flash" + folder = "/home/coder/project" + task_prompt = data.coder_parameter.ai_prompt.value + enable_yolo_mode = true # Auto-approve all tool calls for automation + gemini_system_prompt = <<-EOT + You are a helpful coding assistant. Always explain your code changes clearly. + YOU MUST REPORT ALL TASKS TO CODER. + EOT +} +``` + +> [!WARNING] +> YOLO mode automatically approves all tool calls without user confirmation. The agent has access to your machine's file system and terminal. Only enable in trusted, isolated environments. + +### Using Vertex AI (Enterprise) + +For enterprise users who prefer Google's Vertex AI platform: + +```tf +module "gemini" { + source = "registry.coder.com/coder-labs/gemini/coder" + version = "1.1.0" + agent_id = coder_agent.example.id + gemini_api_key = var.gemini_api_key + folder = "/home/coder/project" + use_vertexai = true +} +``` ## Troubleshooting -- If Gemini CLI is not found, ensure `install_gemini = true` and your API key is valid -- Node.js and npm are installed automatically if missing (using NVM) -- Check logs in `/home/coder/.gemini-module/` for install/start output -- We highly recommend using the `gemini_api_key` variable, this also ensures smooth tasks running without needing to sign in to Google. +- If Gemini CLI is not found, ensure your API key is valid (`install_gemini` defaults to `true`) +- Check logs in `~/.gemini-module/` for install/start output +- Use the `gemini_api_key` variable to avoid requiring Google sign-in -> [!IMPORTANT] -> To use tasks with Gemini CLI, ensure you have the `gemini_api_key` variable set, and **you pass the `AI Prompt` Parameter**. -> By default we inject the "theme": "Default" and "selectedAuthType": "gemini-api-key" to your ~/.gemini/settings.json along with the coder mcp server. -> In `gemini_instruction_prompt` and `AI Prompt` text we recommend using (\`\`) backticks instead of quotes to avoid escaping issues. Eg: gemini_instruction_prompt = "Start every response with \`Gemini says:\` " +The module creates log files in the workspace's `~/.gemini-module` directory for debugging purposes. ## References -- [Gemini CLI Documentation](https://ai.google.dev/gemini-api/docs/cli) +- [Gemini CLI Documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/index.md) - [AgentAPI Documentation](https://github.com/coder/agentapi) -- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) +- [Coder AI Agents Guide](https://coder.com/docs/ai-coder) diff --git a/registry/coder-labs/modules/gemini/main.test.ts b/registry/coder-labs/modules/gemini/main.test.ts index 181b61146..e0a36c499 100644 --- a/registry/coder-labs/modules/gemini/main.test.ts +++ b/registry/coder-labs/modules/gemini/main.test.ts @@ -8,7 +8,6 @@ import { } from "bun:test"; import { execContainer, readFileContainer, runTerraformInit } from "~test"; import { - loadTestFile, writeExecutable, setup as setupUtil, execModuleScript, @@ -54,10 +53,24 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => { agentapiMockScript: props?.agentapiMockScript, }); if (!props?.skipGeminiMock) { + const geminiMockContent = `#!/bin/bash + +if [[ "$1" == "--version" ]]; then + echo "HELLO: $(bash -c env)" + echo "gemini version v2.5.0" + exit 0 +fi + +set -e + +while true; do + echo "$(date) - gemini-mock" + sleep 15 +done`; await writeExecutable({ containerId: id, filePath: "/usr/bin/gemini", - content: await loadTestFile(import.meta.dir, "gemini-mock.sh"), + content: geminiMockContent, }); } return { id }; @@ -70,7 +83,7 @@ describe("gemini", async () => { await runTerraformInit(import.meta.dir); }); - test("happy-path", async () => { + test("agent-api", async () => { const { id } = await setup(); await execModuleScript(id); await expectAgentAPIStarted(id); @@ -117,7 +130,7 @@ describe("gemini", async () => { await execModuleScript(id); const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log"); - expect(resp).toContain("gemini_api_key provided !"); + expect(resp).toContain("Using direct Gemini API with API key"); }); test("use-vertexai", async () => { @@ -197,6 +210,20 @@ describe("gemini", async () => { expect(resp).toContain(prompt); }); + test("task-prompt", async () => { + const taskPrompt = "Create a simple Hello World function"; + const { id } = await setup({ + moduleVariables: { + task_prompt: taskPrompt, + }, + }); + await execModuleScript(id, { + GEMINI_TASK_PROMPT: taskPrompt, + }); + const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log"); + expect(resp).toContain("Running automated task:"); + }); + test("start-without-prompt", async () => { const { id } = await setup(); await execModuleScript(id); diff --git a/registry/coder-labs/modules/gemini/main.tf b/registry/coder-labs/modules/gemini/main.tf index ab4fa9450..ad35d4577 100644 --- a/registry/coder-labs/modules/gemini/main.tf +++ b/registry/coder-labs/modules/gemini/main.tf @@ -74,14 +74,14 @@ variable "use_vertexai" { variable "install_agentapi" { type = bool - description = "Whether to install AgentAPI." + description = "Whether to install AgentAPI for web UI and task automation." default = true } variable "agentapi_version" { type = string description = "The version of AgentAPI to install." - default = "v0.3.0" + default = "v0.2.3" } variable "gemini_model" { @@ -102,12 +102,10 @@ variable "post_install_script" { default = null } -data "coder_parameter" "ai_prompt" { - type = "string" - name = "AI Prompt" +variable "task_prompt" { + type = string + description = "Task prompt for automated Gemini execution" default = "" - description = "Initial prompt for the Gemini CLI" - mutable = true } variable "additional_extensions" { @@ -122,12 +120,24 @@ variable "gemini_system_prompt" { default = "" } +variable "enable_yolo_mode" { + type = bool + description = "Enable YOLO mode to automatically approve all tool calls without user confirmation. Use with caution." + default = false +} + resource "coder_env" "gemini_api_key" { agent_id = var.agent_id name = "GEMINI_API_KEY" value = var.gemini_api_key } +resource "coder_env" "google_api_key" { + agent_id = var.agent_id + name = "GOOGLE_API_KEY" + value = var.gemini_api_key +} + resource "coder_env" "gemini_use_vertex_ai" { agent_id = var.agent_id name = "GOOGLE_GENAI_USE_VERTEXAI" @@ -166,7 +176,7 @@ EOT module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.0.0" + version = "1.0.2" agent_id = var.agent_id web_app_slug = local.app_slug @@ -181,22 +191,7 @@ module "agentapi" { agentapi_version = var.agentapi_version pre_install_script = var.pre_install_script post_install_script = var.post_install_script - start_script = <<-EOT - #!/bin/bash - set -o errexit - set -o pipefail - - echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh - chmod +x /tmp/start.sh - GEMINI_API_KEY='${var.gemini_api_key}' \ - GOOGLE_GENAI_USE_VERTEXAI='${var.use_vertexai}' \ - GEMINI_MODEL='${var.gemini_model}' \ - GEMINI_START_DIRECTORY='${var.folder}' \ - GEMINI_TASK_PROMPT='${base64encode(data.coder_parameter.ai_prompt.value)}' \ - /tmp/start.sh - EOT - - install_script = <<-EOT + install_script = <<-EOT #!/bin/bash set -o errexit set -o pipefail @@ -209,7 +204,23 @@ module "agentapi" { BASE_EXTENSIONS='${base64encode(replace(local.base_extensions, "'", "'\\''"))}' \ ADDITIONAL_EXTENSIONS='${base64encode(replace(var.additional_extensions != null ? var.additional_extensions : "", "'", "'\\''"))}' \ GEMINI_START_DIRECTORY='${var.folder}' \ - GEMINI_INSTRUCTION_PROMPT='${base64encode(var.gemini_system_prompt)}' \ + GEMINI_SYSTEM_PROMPT='${base64encode(var.gemini_system_prompt)}' \ /tmp/install.sh EOT + start_script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + chmod +x /tmp/start.sh + GEMINI_API_KEY='${var.gemini_api_key}' \ + GOOGLE_API_KEY='${var.gemini_api_key}' \ + GOOGLE_GENAI_USE_VERTEXAI='${var.use_vertexai}' \ + GEMINI_YOLO_MODE='${var.enable_yolo_mode}' \ + GEMINI_MODEL='${var.gemini_model}' \ + GEMINI_START_DIRECTORY='${var.folder}' \ + GEMINI_TASK_PROMPT='${var.task_prompt}' \ + /tmp/start.sh + EOT } \ No newline at end of file diff --git a/registry/coder-labs/modules/gemini/scripts/install.sh b/registry/coder-labs/modules/gemini/scripts/install.sh index a800dbd29..89ea07a0b 100644 --- a/registry/coder-labs/modules/gemini/scripts/install.sh +++ b/registry/coder-labs/modules/gemini/scripts/install.sh @@ -2,7 +2,6 @@ BOLD='\033[0;1m' -# Function to check if a command exists command_exists() { command -v "$1" >/dev/null 2>&1 } @@ -12,7 +11,7 @@ set -o nounset ARG_GEMINI_CONFIG=$(echo -n "$ARG_GEMINI_CONFIG" | base64 -d) BASE_EXTENSIONS=$(echo -n "$BASE_EXTENSIONS" | base64 -d) ADDITIONAL_EXTENSIONS=$(echo -n "$ADDITIONAL_EXTENSIONS" | base64 -d) -GEMINI_INSTRUCTION_PROMPT=$(echo -n "$GEMINI_INSTRUCTION_PROMPT" | base64 -d) +GEMINI_SYSTEM_PROMPT=$(echo -n "$GEMINI_SYSTEM_PROMPT" | base64 -d) echo "--------------------------------" printf "gemini_config: %s\n" "$ARG_GEMINI_CONFIG" @@ -23,7 +22,6 @@ echo "--------------------------------" set +o nounset function install_node() { - # borrowed from claude-code module if ! command_exists npm; then printf "npm not found, checking for Node.js installation...\n" if ! command_exists node; then @@ -52,24 +50,15 @@ function install_node() { function install_gemini() { if [ "${ARG_INSTALL}" = "true" ]; then - # we need node to install and run gemini-cli install_node - # If nvm does not exist, we will create a global npm directory (this os to prevent the possibility of EACCESS issues on npm -g) if ! command_exists nvm; then printf "which node: %s\n" "$(which node)" printf "which npm: %s\n" "$(which npm)" - # Create a directory for global packages mkdir -p "$HOME"/.npm-global - - # Configure npm to use it npm config set prefix "$HOME/.npm-global" - - # Add to PATH for current session export PATH="$HOME/.npm-global/bin:$PATH" - - # Add to shell profile for future sessions if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc fi @@ -108,7 +97,6 @@ function append_extensions_to_settings_json() { fi if [ ! -f "$SETTINGS_PATH" ]; then printf "%s does not exist. Creating with merged mcpServers structure.\n" "$SETTINGS_PATH" - # If ADDITIONAL_EXTENSIONS is not set or empty, use '{}' ADD_EXT_JSON='{}' if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS" @@ -116,10 +104,7 @@ function append_extensions_to_settings_json() { printf '{"mcpServers":%s}\n' "$(jq -s 'add' <(echo "$BASE_EXTENSIONS") <(echo "$ADD_EXT_JSON"))" > "$SETTINGS_PATH" fi - # Prepare temp files TMP_SETTINGS=$(mktemp) - - # If ADDITIONAL_EXTENSIONS is not set or empty, use '{}' ADD_EXT_JSON='{}' if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is set.\n" @@ -133,14 +118,13 @@ function append_extensions_to_settings_json() { '.mcpServers = (.mcpServers // {} + $base + $add)' \ "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH" - # Add theme and selectedAuthType fields jq '.theme = "Default" | .selectedAuthType = "gemini-api-key"' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH" printf "[append_extensions_to_settings_json] Merge complete.\n" } -function add_instruction_prompt_if_exists() { - if [ -n "${GEMINI_INSTRUCTION_PROMPT:-}" ]; then +function add_system_prompt_if_exists() { + if [ -n "${GEMINI_SYSTEM_PROMPT:-}" ]; then if [ -d "${GEMINI_START_DIRECTORY}" ]; then printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}" cd "${GEMINI_START_DIRECTORY}" || { @@ -160,16 +144,21 @@ function add_instruction_prompt_if_exists() { fi touch GEMINI.md printf "Setting GEMINI.md\n" - echo "${GEMINI_INSTRUCTION_PROMPT}" > GEMINI.md + echo "${GEMINI_SYSTEM_PROMPT}" > GEMINI.md else printf "GEMINI.md is not set.\n" fi } +function configure_mcp() { + export CODER_MCP_APP_STATUS_SLUG="gemini" + export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" + coder exp mcp configure gemini "${GEMINI_START_DIRECTORY}" +} -# Install Gemini install_gemini gemini --version populate_settings_json -add_instruction_prompt_if_exists +add_system_prompt_if_exists +configure_mcp diff --git a/registry/coder-labs/modules/gemini/scripts/start.sh b/registry/coder-labs/modules/gemini/scripts/start.sh index 00e0b5edb..aa09c165f 100644 --- a/registry/coder-labs/modules/gemini/scripts/start.sh +++ b/registry/coder-labs/modules/gemini/scripts/start.sh @@ -1,6 +1,7 @@ #!/bin/bash +set -o errexit +set -o pipefail -# Load shell environment source "$HOME"/.bashrc command_exists() { @@ -15,7 +16,8 @@ fi printf "Version: %s\n" "$(gemini --version)" -GEMINI_TASK_PROMPT=$(echo -n "$GEMINI_TASK_PROMPT" | base64 -d) +MODULE_DIR="$HOME/.gemini-module" +mkdir -p "$MODULE_DIR" if command_exists gemini; then printf "Gemini is installed\n" @@ -43,20 +45,30 @@ else fi if [ -n "$GEMINI_TASK_PROMPT" ]; then - printf "Running the task prompt %s\n" "$GEMINI_TASK_PROMPT" + printf "Running automated task: %s\n" "$GEMINI_TASK_PROMPT" PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GEMINI_TASK_PROMPT" + PROMPT_FILE="$MODULE_DIR/prompt.txt" + echo -n "$PROMPT" >"$PROMPT_FILE" GEMINI_ARGS=(--prompt-interactive "$PROMPT") else - printf "No task prompt given.\n" + printf "Starting Gemini CLI in interactive mode.\n" GEMINI_ARGS=() fi -if [ -n "$GEMINI_API_KEY" ]; then - printf "gemini_api_key provided !\n" +if [ -n "$GEMINI_YOLO_MODE" ] && [ "$GEMINI_YOLO_MODE" = "true" ]; then + printf "YOLO mode enabled - will auto-approve all tool calls\n" + GEMINI_ARGS+=(--yolo) +fi + +if [ -n "$GEMINI_API_KEY" ] || [ -n "$GOOGLE_API_KEY" ]; then + if [ -n "$GOOGLE_GENAI_USE_VERTEXAI" ] && [ "$GOOGLE_GENAI_USE_VERTEXAI" = "true" ]; then + printf "Using Vertex AI with API key\n" + else + printf "Using direct Gemini API with API key\n" + fi else - printf "gemini_api_key not provided\n" + printf "No API key provided (neither GEMINI_API_KEY nor GOOGLE_API_KEY)\n" fi -# use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters -# are visible in the terminal screen by default. -agentapi server --term-width 67 --term-height 1190 -- gemini "${GEMINI_ARGS[@]}" \ No newline at end of file +agentapi server --term-width 67 --term-height 1190 -- \ + bash -c "$(printf '%q ' gemini "${GEMINI_ARGS[@]}")" \ No newline at end of file diff --git a/registry/coder-labs/modules/gemini/testdata/gemini-mock.sh b/registry/coder-labs/modules/gemini/testdata/gemini-mock.sh deleted file mode 100644 index 53c9c41de..000000000 --- a/registry/coder-labs/modules/gemini/testdata/gemini-mock.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [[ "$1" == "--version" ]]; then - echo "HELLO: $(bash -c env)" - echo "gemini version v2.5.0" - exit 0 -fi - -set -e - -while true; do - echo "$(date) - gemini-mock" - sleep 15 -done \ No newline at end of file