Skip to content

Commit c2a0c52

Browse files
authored
Merge branch 'main' into aws-snapshot
2 parents 49e7b51 + f89ea12 commit c2a0c52

File tree

8 files changed

+754
-2
lines changed

8 files changed

+754
-2
lines changed

.github/workflows/deploy-registry.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ jobs:
2626
- name: Checkout code
2727
uses: actions/checkout@v4
2828
- name: Authenticate with Google Cloud
29-
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193
29+
uses: google-github-actions/auth@140bb5113ffb6b65a7e9b937a81fa96cf5064462
3030
with:
3131
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
3232
service_account: [email protected]
3333
- name: Set up Google Cloud SDK
34-
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
34+
uses: google-github-actions/setup-gcloud@6a7c903a70c8625ed6700fa299f5ddb4ca6022e9
3535
- name: Deploy to dev.registry.coder.com
3636
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
3737
- name: Deploy to registry.coder.com

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