Skip to content

feat: gemini cli module #246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
36199a7
feat: gemini module
35C4n0r Jul 21, 2025
feb7051
feat: add README.md
35C4n0r Jul 21, 2025
1968f9c
feat: bun fmt
35C4n0r Jul 21, 2025
a767299
feat: update tests
35C4n0r Jul 21, 2025
e81c651
feat: gemini api key
35C4n0r Jul 21, 2025
6f9d65c
Merge branch 'main' into feat-gemini-cli
35C4n0r Jul 21, 2025
21f06a4
feat: update README.md
35C4n0r Jul 21, 2025
6d000e4
feat: add support for reporting tasks
35C4n0r Jul 23, 2025
89b1da1
Merge branch 'main' into feat-gemini-cli
35C4n0r Jul 23, 2025
2be7739
Update registry/anomaly/modules/gemini/README.md
35C4n0r Jul 23, 2025
528907e
feat: enhance README and start script for improved task reporting
35C4n0r Jul 23, 2025
086597d
Merge branch 'main' into feat-gemini-cli
35C4n0r Jul 23, 2025
88696ad
fmt: bun fmt
35C4n0r Jul 23, 2025
164083a
fea: update readme to IMPORTANT
35C4n0r Jul 24, 2025
90485f8
Update registry/anomaly/modules/gemini/scripts/start.sh
35C4n0r Jul 24, 2025
4409901
Update registry/anomaly/modules/gemini/main.tf
35C4n0r Jul 24, 2025
b95a68e
Update registry/anomaly/modules/gemini/main.tf
35C4n0r Jul 24, 2025
c8d0a37
refactor: rename variables for clarity in main.tf
35C4n0r Jul 24, 2025
9f06c1c
fix: update default folder path for Gemini configuration
35C4n0r Jul 24, 2025
c9fbada
fix: use HOME variable for user-specific settings path in install.sh
35C4n0r Jul 24, 2025
6e64e43
fix: fix README.md
35C4n0r Jul 24, 2025
b444aaf
feat: update tests
35C4n0r Jul 24, 2025
8d33768
Merge branch 'main' into feat-gemini-cli
DevelopmentCats Jul 25, 2025
02504ef
feat: fix image format
35C4n0r Jul 25, 2025
353c4b9
feat: fix image format
35C4n0r Jul 25, 2025
880137c
fix: correct formatting in tmux README.md
35C4n0r Jul 25, 2025
db20aa5
feat: move gemini module to coder-labs
35C4n0r Jul 25, 2025
0ac5440
Update README.md
35C4n0r Jul 28, 2025
289b84e
Revert "fix: correct formatting in tmux README.md"
35C4n0r Jul 28, 2025
9e14d55
Revert "feat: fix image format"
35C4n0r Jul 28, 2025
f4fe7c4
Revert "feat: fix image format"
35C4n0r Jul 28, 2025
dc39170
feat: b64encode inputs to script
35C4n0r Jul 28, 2025
919eb38
feat: remove comment
35C4n0r Jul 28, 2025
954b713
Merge branch 'main' into feat-gemini-cli
35C4n0r Jul 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .icons/gemini.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions registry/coder-labs/modules/gemini/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
display_name: Gemini CLI
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.

```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"
}
```

## 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

## Usage Example

- Example 1:

```tf
variable "gemini_api_key" {
type = string
description = "Gemini API key"
sensitive = true
}

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:`"
}
```

## How it Works

- **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)

## 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.

> [!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:\` "

## References

- [Gemini CLI Documentation](https://ai.google.dev/gemini-api/docs/cli)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
207 changes: 207 additions & 0 deletions registry/coder-labs/modules/gemini/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";

let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});

interface SetupProps {
skipAgentAPIMock?: boolean;
skipGeminiMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}

const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_gemini: props?.skipGeminiMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
gemini_model: "test-model",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipGeminiMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/gemini",
content: await loadTestFile(import.meta.dir, "gemini-mock.sh"),
});
}
return { id };
};

setDefaultTimeout(60 * 1000);

describe("gemini", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});

test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});

test("install-gemini-version", async () => {
const version_to_install = "0.1.13";
const { id } = await setup({
skipGeminiMock: true,
moduleVariables: {
install_gemini: "true",
gemini_version: version_to_install,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.gemini-module/install.log || true`,
]);
expect(resp.stdout).toContain(version_to_install);
});

test("gemini-settings-json", async () => {
const settings = '{"foo": "bar"}';
const { id } = await setup({
moduleVariables: {
gemini_settings_json: settings,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
expect(resp).toContain("foo");
expect(resp).toContain("bar");
});

test("gemini-api-key", async () => {
const apiKey = "test-api-key-123";
const { id } = await setup({
moduleVariables: {
gemini_api_key: apiKey,
},
});
await execModuleScript(id);

const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
expect(resp).toContain("gemini_api_key provided !");
});

test("use-vertexai", async () => {
const { id } = await setup({
skipGeminiMock: false,
moduleVariables: {
use_vertexai: "true",
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
expect(resp).toContain('GOOGLE_GENAI_USE_VERTEXAI=\'true\'');
});

test("gemini-model", async () => {
const model = "gemini-2.5-pro";
const { id } = await setup({
skipGeminiMock: false,
moduleVariables: {
gemini_model: model,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
expect(resp).toContain(model);
});

test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
post_install_script: "#!/bin/bash\necho 'post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/pre_install.log");
expect(preInstallLog).toContain("pre-install-script");
const postInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/post_install.log");
expect(postInstallLog).toContain("post-install-script");
});

test("folder-variable", async () => {
const folder = "/tmp/gemini-test-folder";
const { id } = await setup({
skipGeminiMock: false,
moduleVariables: {
folder,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
expect(resp).toContain(folder);
});

test("additional-extensions", async () => {
const additional = '{"custom": {"enabled": true}}';
const { id } = await setup({
moduleVariables: {
additional_extensions: additional,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
expect(resp).toContain("custom");
expect(resp).toContain("enabled");
});

test("gemini-system-prompt", async () => {
const prompt = "This is a system prompt for Gemini.";
const { id } = await setup({
moduleVariables: {
gemini_system_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/GEMINI.md");
expect(resp).toContain(prompt);
});

test("start-without-prompt", async () => {
const { id } = await setup();
await execModuleScript(id);
const prompt = await execContainer(id, ["ls", "-l", "/home/coder/GEMINI.md"]);
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
});
Loading