Skip to content

Commit f89ea12

Browse files
35C4n0rmatifalihugodutkaDevelopmentCats
authored
feat: gemini cli module (#246)
Closes #237 /claim #237 ## Description ~https://www.loom.com/share/5b099c73935f4f87b8fdafe1509bb79d?sid=7fea43d6-86e9-45ae-9892-efeb3c820b82~ Updated: https://www.loom.com/share/62e907eae8544d8cbbe560d7f63bd02d?sid=201212fa-eb58-44b5-8706-8bf9c2c37433 <!-- Briefly describe what this PR does and why --> ## Type of Change - [x] New module - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information <!-- Delete this section if not applicable --> **Path:** `registry/coder-labs/modules/gemini` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally ## Related Issues <!-- Link related issues or write "None" if not applicable --> --------- Co-authored-by: Atif Ali <me@matifali.dev> Co-authored-by: Hugo Dutka <dutkahugo@gmail.com> Co-authored-by: DevCats <christofer@coder.com>
1 parent 0fe4794 commit f89ea12

File tree

7 files changed

+752
-0
lines changed

7 files changed

+752
-0
lines changed

.icons/gemini.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
display_name: Gemini CLI
3+
icon: ../../../../.icons/gemini.svg
4+
description: Run Gemini CLI in your workspace with AgentAPI integration
5+
verified: true
6+
tags: [agent, gemini, ai, google, tasks]
7+
---
8+
9+
# Gemini CLI
10+
11+
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.
12+
13+
```tf
14+
module "gemini" {
15+
source = "registry.coder.com/coder-labs/gemini/coder"
16+
version = "1.0.0"
17+
agent_id = coder_agent.example.id
18+
gemini_api_key = var.gemini_api_key
19+
gemini_model = "gemini-2.5-pro"
20+
install_gemini = true
21+
gemini_version = "latest"
22+
agentapi_version = "latest"
23+
}
24+
```
25+
26+
## Prerequisites
27+
28+
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module to your template
29+
- Node.js and npm will be installed automatically if not present
30+
31+
## Usage Example
32+
33+
- Example 1:
34+
35+
```tf
36+
variable "gemini_api_key" {
37+
type = string
38+
description = "Gemini API key"
39+
sensitive = true
40+
}
41+
42+
module "gemini" {
43+
count = data.coder_workspace.me.start_count
44+
source = "registry.coder.com/coder-labs/gemini/coder"
45+
version = "1.0.0"
46+
agent_id = coder_agent.example.id
47+
gemini_api_key = var.gemini_api_key # we recommend providing this parameter inorder to have a smoother experience (i.e. no google sign-in)
48+
gemini_model = "gemini-2.5-flash"
49+
install_gemini = true
50+
gemini_version = "latest"
51+
gemini_instruction_prompt = "Start every response with `Gemini says:`"
52+
}
53+
```
54+
55+
## How it Works
56+
57+
- **Install**: The module installs Gemini CLI using npm (installs Node.js via NVM if needed)
58+
- **Instruction Prompt**: If `GEMINI_INSTRUCTION_PROMPT` and `GEMINI_START_DIRECTORY` are set, creates the directory (if needed) and writes the prompt to `GEMINI.md`
59+
- **Start**: Launches Gemini CLI in the specified directory, wrapped by AgentAPI
60+
- **Environment**: Sets `GEMINI_API_KEY`, `GOOGLE_GENAI_USE_VERTEXAI`, `GEMINI_MODEL` for the CLI (if variables provided)
61+
62+
## Troubleshooting
63+
64+
- If Gemini CLI is not found, ensure `install_gemini = true` and your API key is valid
65+
- Node.js and npm are installed automatically if missing (using NVM)
66+
- Check logs in `/home/coder/.gemini-module/` for install/start output
67+
- We highly recommend using the `gemini_api_key` variable, this also ensures smooth tasks running without needing to sign in to Google.
68+
69+
> [!IMPORTANT]
70+
> To use tasks with Gemini CLI, ensure you have the `gemini_api_key` variable set, and **you pass the `AI Prompt` Parameter**.
71+
> By default we inject the "theme": "Default" and "selectedAuthType": "gemini-api-key" to your ~/.gemini/settings.json along with the coder mcp server.
72+
> 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:\` "
73+
74+
## References
75+
76+
- [Gemini CLI Documentation](https://ai.google.dev/gemini-api/docs/cli)
77+
- [AgentAPI Documentation](https://github.com/coder/agentapi)
78+
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import {
2+
test,
3+
afterEach,
4+
describe,
5+
setDefaultTimeout,
6+
beforeAll,
7+
expect,
8+
} from "bun:test";
9+
import { execContainer, readFileContainer, runTerraformInit } from "~test";
10+
import {
11+
loadTestFile,
12+
writeExecutable,
13+
setup as setupUtil,
14+
execModuleScript,
15+
expectAgentAPIStarted,
16+
} from "../../../coder/modules/agentapi/test-util";
17+
18+
let cleanupFunctions: (() => Promise<void>)[] = [];
19+
const registerCleanup = (cleanup: () => Promise<void>) => {
20+
cleanupFunctions.push(cleanup);
21+
};
22+
afterEach(async () => {
23+
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
24+
cleanupFunctions = [];
25+
for (const cleanup of cleanupFnsCopy) {
26+
try {
27+
await cleanup();
28+
} catch (error) {
29+
console.error("Error during cleanup:", error);
30+
}
31+
}
32+
});
33+
34+
interface SetupProps {
35+
skipAgentAPIMock?: boolean;
36+
skipGeminiMock?: boolean;
37+
moduleVariables?: Record<string, string>;
38+
agentapiMockScript?: string;
39+
}
40+
41+
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
42+
const projectDir = "/home/coder/project";
43+
const { id } = await setupUtil({
44+
moduleDir: import.meta.dir,
45+
moduleVariables: {
46+
install_gemini: props?.skipGeminiMock ? "true" : "false",
47+
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
48+
gemini_model: "test-model",
49+
...props?.moduleVariables,
50+
},
51+
registerCleanup,
52+
projectDir,
53+
skipAgentAPIMock: props?.skipAgentAPIMock,
54+
agentapiMockScript: props?.agentapiMockScript,
55+
});
56+
if (!props?.skipGeminiMock) {
57+
await writeExecutable({
58+
containerId: id,
59+
filePath: "/usr/bin/gemini",
60+
content: await loadTestFile(import.meta.dir, "gemini-mock.sh"),
61+
});
62+
}
63+
return { id };
64+
};
65+
66+
setDefaultTimeout(60 * 1000);
67+
68+
describe("gemini", async () => {
69+
beforeAll(async () => {
70+
await runTerraformInit(import.meta.dir);
71+
});
72+
73+
test("happy-path", async () => {
74+
const { id } = await setup();
75+
await execModuleScript(id);
76+
await expectAgentAPIStarted(id);
77+
});
78+
79+
test("install-gemini-version", async () => {
80+
const version_to_install = "0.1.13";
81+
const { id } = await setup({
82+
skipGeminiMock: true,
83+
moduleVariables: {
84+
install_gemini: "true",
85+
gemini_version: version_to_install,
86+
},
87+
});
88+
await execModuleScript(id);
89+
const resp = await execContainer(id, [
90+
"bash",
91+
"-c",
92+
`cat /home/coder/.gemini-module/install.log || true`,
93+
]);
94+
expect(resp.stdout).toContain(version_to_install);
95+
});
96+
97+
test("gemini-settings-json", async () => {
98+
const settings = '{"foo": "bar"}';
99+
const { id } = await setup({
100+
moduleVariables: {
101+
gemini_settings_json: settings,
102+
},
103+
});
104+
await execModuleScript(id);
105+
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
106+
expect(resp).toContain("foo");
107+
expect(resp).toContain("bar");
108+
});
109+
110+
test("gemini-api-key", async () => {
111+
const apiKey = "test-api-key-123";
112+
const { id } = await setup({
113+
moduleVariables: {
114+
gemini_api_key: apiKey,
115+
},
116+
});
117+
await execModuleScript(id);
118+
119+
const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
120+
expect(resp).toContain("gemini_api_key provided !");
121+
});
122+
123+
test("use-vertexai", async () => {
124+
const { id } = await setup({
125+
skipGeminiMock: false,
126+
moduleVariables: {
127+
use_vertexai: "true",
128+
},
129+
});
130+
await execModuleScript(id);
131+
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
132+
expect(resp).toContain('GOOGLE_GENAI_USE_VERTEXAI=\'true\'');
133+
});
134+
135+
test("gemini-model", async () => {
136+
const model = "gemini-2.5-pro";
137+
const { id } = await setup({
138+
skipGeminiMock: false,
139+
moduleVariables: {
140+
gemini_model: model,
141+
},
142+
});
143+
await execModuleScript(id);
144+
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
145+
expect(resp).toContain(model);
146+
});
147+
148+
test("pre-post-install-scripts", async () => {
149+
const { id } = await setup({
150+
moduleVariables: {
151+
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
152+
post_install_script: "#!/bin/bash\necho 'post-install-script'",
153+
},
154+
});
155+
await execModuleScript(id);
156+
const preInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/pre_install.log");
157+
expect(preInstallLog).toContain("pre-install-script");
158+
const postInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/post_install.log");
159+
expect(postInstallLog).toContain("post-install-script");
160+
});
161+
162+
test("folder-variable", async () => {
163+
const folder = "/tmp/gemini-test-folder";
164+
const { id } = await setup({
165+
skipGeminiMock: false,
166+
moduleVariables: {
167+
folder,
168+
},
169+
});
170+
await execModuleScript(id);
171+
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
172+
expect(resp).toContain(folder);
173+
});
174+
175+
test("additional-extensions", async () => {
176+
const additional = '{"custom": {"enabled": true}}';
177+
const { id } = await setup({
178+
moduleVariables: {
179+
additional_extensions: additional,
180+
},
181+
});
182+
await execModuleScript(id);
183+
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
184+
expect(resp).toContain("custom");
185+
expect(resp).toContain("enabled");
186+
});
187+
188+
test("gemini-system-prompt", async () => {
189+
const prompt = "This is a system prompt for Gemini.";
190+
const { id } = await setup({
191+
moduleVariables: {
192+
gemini_system_prompt: prompt,
193+
},
194+
});
195+
await execModuleScript(id);
196+
const resp = await readFileContainer(id, "/home/coder/GEMINI.md");
197+
expect(resp).toContain(prompt);
198+
});
199+
200+
test("start-without-prompt", async () => {
201+
const { id } = await setup();
202+
await execModuleScript(id);
203+
const prompt = await execContainer(id, ["ls", "-l", "/home/coder/GEMINI.md"]);
204+
expect(prompt.exitCode).not.toBe(0);
205+
expect(prompt.stderr).toContain("No such file or directory");
206+
});
207+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy