-
Notifications
You must be signed in to change notification settings - Fork 49
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
Merged
+752
−0
Merged
Changes from 27 commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
36199a7
feat: gemini module
35C4n0r feb7051
feat: add README.md
35C4n0r 1968f9c
feat: bun fmt
35C4n0r a767299
feat: update tests
35C4n0r e81c651
feat: gemini api key
35C4n0r 6f9d65c
Merge branch 'main' into feat-gemini-cli
35C4n0r 21f06a4
feat: update README.md
35C4n0r 6d000e4
feat: add support for reporting tasks
35C4n0r 89b1da1
Merge branch 'main' into feat-gemini-cli
35C4n0r 2be7739
Update registry/anomaly/modules/gemini/README.md
35C4n0r 528907e
feat: enhance README and start script for improved task reporting
35C4n0r 086597d
Merge branch 'main' into feat-gemini-cli
35C4n0r 88696ad
fmt: bun fmt
35C4n0r 164083a
fea: update readme to IMPORTANT
35C4n0r 90485f8
Update registry/anomaly/modules/gemini/scripts/start.sh
35C4n0r 4409901
Update registry/anomaly/modules/gemini/main.tf
35C4n0r b95a68e
Update registry/anomaly/modules/gemini/main.tf
35C4n0r c8d0a37
refactor: rename variables for clarity in main.tf
35C4n0r 9f06c1c
fix: update default folder path for Gemini configuration
35C4n0r c9fbada
fix: use HOME variable for user-specific settings path in install.sh
35C4n0r 6e64e43
fix: fix README.md
35C4n0r b444aaf
feat: update tests
35C4n0r 8d33768
Merge branch 'main' into feat-gemini-cli
DevelopmentCats 02504ef
feat: fix image format
35C4n0r 353c4b9
feat: fix image format
35C4n0r 880137c
fix: correct formatting in tmux README.md
35C4n0r db20aa5
feat: move gemini module to coder-labs
35C4n0r 0ac5440
Update README.md
35C4n0r 289b84e
Revert "fix: correct formatting in tmux README.md"
35C4n0r 9e14d55
Revert "feat: fix image format"
35C4n0r f4fe7c4
Revert "feat: fix image format"
35C4n0r dc39170
feat: b64encode inputs to script
35C4n0r 919eb38
feat: remove comment
35C4n0r 954b713
Merge branch 'main' into feat-gemini-cli
35C4n0r File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
35C4n0r marked this conversation as resolved.
Show resolved
Hide resolved
|
||
--- | ||
|
||
# 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import { | ||
test, | ||
afterEach, | ||
describe, | ||
setDefaultTimeout, | ||
beforeAll, | ||
expect, | ||
} from "bun:test"; | ||
import { execContainer, findResourceInstance, 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); | ||
// const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log"); | ||
// console.error("\n\n\nINSTALL_LOG => ", resp, "\n\n\n") | ||
// | ||
// const resp2 = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log"); | ||
// console.error("\n\n\nSTART_LOG => ", resp2, "\n\n\n") | ||
35C4n0r marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
test("install-gemini-version", async () => { | ||
const { id } = await setup({ | ||
moduleVariables: { | ||
install_gemini: "true", | ||
gemini_version: "v2.5.0", | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
// Check for version in install log or binary (customize as needed) | ||
35C4n0r marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const resp = await execContainer(id, [ | ||
"bash", | ||
"-c", | ||
`cat /home/coder/.gemini-module/install.log || true`, | ||
]); | ||
expect(resp.stdout).toContain("v2.5.0"); | ||
}); | ||
// | ||
35C4n0r marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
// Check that the folder is used (e.g., by checking a file or env) | ||
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"); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.