antigravity-cli: rename gemini-cli module

This commit is contained in:
Austin Horstman
2026-05-20 09:07:07 -05:00
parent 0450574818
commit ff77a72fe5
43 changed files with 1121 additions and 363 deletions

View File

@@ -0,0 +1,498 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.programs.antigravity-cli;
jsonFormat = pkgs.formats.json { };
tomlFormat = pkgs.formats.toml { };
isStorePathString =
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
isPathLikeContent = content: lib.isPath content || isStorePathString content;
transformMcpServer =
server:
removeAttrs server [
"httpUrl"
"url"
]
// lib.optionalAttrs (server ? httpUrl) {
serverUrl = server.httpUrl;
}
// lib.optionalAttrs (server ? url) {
serverUrl = server.url;
};
transformMcpServers = lib.mapAttrs (_name: transformMcpServer);
commandSkillName = lib.replaceStrings [ "/" ] [ ":" ];
in
{
meta.maintainers = [ lib.maintainers.rrvsh ];
imports = [
(lib.mkRenamedOptionModule [ "programs" "gemini-cli" ] [ "programs" "antigravity-cli" ])
];
options.programs.antigravity-cli = {
enable = lib.mkEnableOption "Antigravity CLI";
package = lib.mkPackageOption pkgs "antigravity-cli" {
nullable = true;
};
useLegacyGeminiConfig = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to write configuration to the legacy Gemini CLI locations.
This is enabled automatically when
{option}`programs.antigravity-cli.package` is a `gemini-cli` package.
'';
};
enableMcpIntegration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to integrate the MCP servers config from
{option}`programs.mcp.servers` into
{option}`programs.antigravity-cli.mcpServers`.
Note: Any servers already present in
{option}`programs.antigravity-cli.mcpServers`
is not overridden by servers present under the same
name in {option}`programs.mcp.servers`
'';
};
settings = lib.mkOption {
inherit (jsonFormat) type;
default = { };
example = {
colorScheme = "tokyo night";
altScreenMode = "always";
toolPermission = "proceed-in-sandbox";
artifactReviewPolicy = "agent-decides";
};
description = ''
Configuration written to
{file}`~/.gemini/antigravity-cli/settings.json`.
'';
};
mcpServers = lib.mkOption {
inherit (jsonFormat) type;
default = { };
example = {
github = {
serverUrl = "https://api.githubcopilot.com/mcp/";
};
filesystem = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-filesystem"
"/tmp"
];
};
};
description = ''
MCP server configuration written to
{file}`~/.gemini/config/mcp_config.json`.
Remote servers use `serverUrl`; `url` and `httpUrl` are accepted
for migration and converted automatically.
'';
};
commands =
let
commandType = lib.types.submodule {
freeformType = lib.types.str;
options = {
prompt = lib.mkOption {
type = lib.types.str;
description = ''
The prompt that will be sent to the Gemini model when the command is executed.
This can be a single-line or multi-line string.
The special placeholder {{args}} will be replaced with the text the user typed after the command name.
'';
};
description = lib.mkOption {
type = lib.types.str;
description = ''
A brief, one-line description of what the command does.
This text will be displayed next to your command in the /help menu.
If you omit this field, a generic description will be generated from the filename.
'';
};
};
};
in
lib.mkOption {
type = lib.types.attrsOf commandType;
default = { };
description = ''
An attribute set of Gemini CLI custom commands to migrate to
Antigravity CLI global skills.
The name of the attribute set will be the name of each generated
skill directory.
'';
example = lib.literalExpression ''
changelog = {
prompt =
'''
Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.
''';
description = "Adds a new entry to the project's CHANGELOG.md file.";
};
"git/fix" = { # Becomes /git:fix
prompt = "Please analyze the staged git changes and provide a code fix for the issue described here: {{args}}.";
description = "Generates a fix for a given GitHub issue.";
};
'';
};
permissions = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = {
allow = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Permissions to allow without prompting.
'';
};
deny = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Permissions to deny without prompting.
'';
};
ask = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Permissions to ask before using.
'';
};
};
}
);
default = null;
description = ''
Antigravity CLI fine-grained permissions written to
{option}`programs.antigravity-cli.settings.permissions`.
'';
example = lib.literalExpression ''
{
allow = [ "command(git)" ];
deny = [ "command(rm -rf)" ];
ask = [ "command(*)" ];
}
'';
};
policies = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.path tomlFormat.type);
default = { };
description = ''
An attribute set of Gemini CLI policy definitions to create in
{file}`~/.gemini/policies/` when
{option}`programs.antigravity-cli.package` is a `gemini-cli`
package.
Antigravity CLI configures permissions in
{option}`programs.antigravity-cli.settings.permissions` or
{option}`programs.antigravity-cli.permissions`.
'';
example = lib.literalExpression ''
{
"my-rules" = {
rule = [
{
toolName = "run_shell_command";
commandPrefix = "git ";
decision = "ask_user";
priority = 100;
}
];
};
"other-rules" = ./path/to/rules.toml;
}
'';
};
defaultModel = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "gemini-2.5-flash";
description = ''
The default model to use for the CLI.
Will be set as $GEMINI_MODEL when configured.
'';
};
context = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
default = { };
example = lib.literalExpression ''
{
GEMINI = '''
# Global Context
You are a helpful AI assistant for software development.
## Coding Standards
- Follow consistent code style
- Write clear comments
- Test your changes
''';
AGENTS = ./path/to/agents.md;
CONTEXT = '''
Additional context instructions here.
''';
}
'';
description = ''
Global context(s) for Antigravity CLI.
The attribute name becomes the filename, with a {file}`.md`
extension added automatically. Each value is either:
- Inline content as a string
- A path to a file containing the content
The configured files are written to {file}`~/.gemini/`.
Note: You can customize which context file names Antigravity CLI looks for by setting
`settings.context.fileName`. For example:
```nix
settings = {
context.fileName = ["AGENTS.md", "CONTEXT.md", "GEMINI.md"];
};
```
'';
};
skills = lib.mkOption {
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
default = { };
example = lib.literalExpression ''
{
xlsx = ./skills/xlsx/SKILL.md;
data-analysis = ./skills/data-analysis;
pdf-processing = '''
---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
---
# PDF Processing
## Quick start
Use pdfplumber to extract text from PDFs:
```python
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
text = pdf.pages[0].extract_text()
```
''';
}
'';
description = ''
Custom skills for Antigravity CLI.
This option can be either:
- An attribute set defining skills
- A path to a directory containing skill folders
If an attribute set is used, the attribute name becomes the
skill directory name, and the value is either:
- Inline content as a string (creates `~/.gemini/config/skills/<name>/SKILL.md`)
- A path to a file (creates `~/.gemini/config/skills/<name>/SKILL.md`)
- A path to a directory (symlinks `~/.gemini/config/skills/<name>/` to that directory)
If a path is used, it is expected to contain one folder per
skill name, each containing a {file}`SKILL.md`. The directory is
symlinked to {file}`~/.gemini/config/skills/`.
'';
};
};
config =
let
useGeminiConfig =
cfg.useLegacyGeminiConfig || (cfg.package != null && lib.getName cfg.package == "gemini-cli");
antigravitySkillsDir = ".gemini/config/skills";
geminiSkillsDir = ".gemini/skills";
antigravitySettings = lib.recursiveUpdate (lib.optionalAttrs (cfg.permissions != null) {
inherit (cfg) permissions;
}) (removeAttrs cfg.settings [ "mcpServers" ]);
legacyMcpServers = lib.optionalAttrs (cfg.settings ? mcpServers) cfg.settings.mcpServers;
mcpServers = lib.recursiveUpdate legacyMcpServers cfg.mcpServers;
geminiSettings =
lib.recursiveUpdate
(lib.optionalAttrs (cfg.permissions != null) {
inherit (cfg) permissions;
})
(lib.recursiveUpdate cfg.settings (lib.optionalAttrs (mcpServers != { }) { inherit mcpServers; }));
in
lib.mkIf cfg.enable (
lib.mkMerge [
{
home = {
packages = lib.mkIf (cfg.package != null) [ cfg.package ];
sessionVariables = lib.mkIf (cfg.defaultModel != null) {
GEMINI_MODEL = cfg.defaultModel;
};
};
}
{
home.file = lib.mapAttrs' (
n: v: lib.nameValuePair ".gemini/${n}.md" (if lib.isPath v then { source = v; } else { text = v; })
) cfg.context;
}
(lib.mkIf useGeminiConfig {
programs.antigravity-cli.settings.mcpServers = lib.mkIf (
cfg.enableMcpIntegration && config.programs.mcp.enable
) (lib.mapAttrs (_n: lib.mkDefault) config.programs.mcp.servers);
home.file =
lib.optionalAttrs (geminiSettings != { }) {
".gemini/settings.json".source = jsonFormat.generate "gemini-cli-settings.json" geminiSettings;
}
// lib.mapAttrs' (
n: v:
lib.nameValuePair ".gemini/commands/${n}.toml" {
source = tomlFormat.generate "gemini-cli-command-${n}.toml" {
inherit (v) description prompt;
};
}
) cfg.commands
// lib.mapAttrs' (
n: v:
lib.nameValuePair ".gemini/policies/${n}.toml" {
source =
if isPathLikeContent v then v else tomlFormat.generate "gemini-cli-policy-${n}.toml" v;
}
) cfg.policies
// (
if isPathLikeContent cfg.skills then
{
"${geminiSkillsDir}" = {
source = cfg.skills;
recursive = true;
};
}
else
lib.mapAttrs' (
n: v:
if isPathLikeContent v && lib.pathIsDirectory v then
lib.nameValuePair "${geminiSkillsDir}/${n}" {
source = v;
recursive = true;
}
else
lib.nameValuePair "${geminiSkillsDir}/${n}/SKILL.md" (
if isPathLikeContent v then { source = v; } else { text = v; }
)
) cfg.skills
);
})
(lib.mkIf (!useGeminiConfig) {
programs.antigravity-cli.mcpServers =
lib.mkIf (cfg.enableMcpIntegration && config.programs.mcp.enable)
(lib.mapAttrs (_n: server: lib.mkDefault (transformMcpServer server)) config.programs.mcp.servers);
assertions = [
{
assertion = cfg.policies == { };
message = ''
`programs.antigravity-cli.policies` is only supported when
`programs.antigravity-cli.package` is a `gemini-cli` package.
Antigravity CLI configures permissions in
`programs.antigravity-cli.settings.permissions` or
`programs.antigravity-cli.permissions`.
'';
}
];
home.file =
lib.optionalAttrs (antigravitySettings != { }) {
".gemini/antigravity-cli/settings.json".source =
jsonFormat.generate "antigravity-cli-settings.json" antigravitySettings;
}
// lib.optionalAttrs (mcpServers != { }) {
".gemini/config/mcp_config.json".source = jsonFormat.generate "antigravity-cli-mcp-config.json" {
mcpServers = transformMcpServers mcpServers;
};
}
// lib.mapAttrs' (
n: v:
let
skillName = commandSkillName n;
in
lib.nameValuePair "${antigravitySkillsDir}/${skillName}/SKILL.md" {
text = ''
---
name: ${skillName}
description: ${v.description}
---
${v.prompt}
'';
}
) cfg.commands
// (
if isPathLikeContent cfg.skills then
{
"${antigravitySkillsDir}" = {
source = cfg.skills;
recursive = true;
};
}
else
lib.mapAttrs' (
n: v:
if isPathLikeContent v && lib.pathIsDirectory v then
lib.nameValuePair "${antigravitySkillsDir}/${n}" {
source = v;
recursive = true;
}
else
lib.nameValuePair "${antigravitySkillsDir}/${n}/SKILL.md" (
if isPathLikeContent v then { source = v; } else { text = v; }
)
) cfg.skills
);
})
{
assertions = [
{
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
message = "`programs.antigravity-cli.skills` must be a directory when set to a path";
}
];
}
]
);
}

View File

@@ -1,314 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.programs.gemini-cli;
jsonFormat = pkgs.formats.json { };
tomlFormat = pkgs.formats.toml { };
in
{
meta.maintainers = [ lib.maintainers.rrvsh ];
options.programs.gemini-cli = {
enable = lib.mkEnableOption "gemini-cli";
package = lib.mkPackageOption pkgs "gemini-cli" { nullable = true; };
enableMcpIntegration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to integrate the MCP servers config from
{option}`programs.mcp.servers` into
{option}`programs.gemini-cli.settings.mcpServers`.
Note: Any servers already present in
{option}`programs.gemini-cli.settings.mcpServers`
is not overridden by servers present under the same
name in {option}`programs.mcp.servers`
'';
};
settings = lib.mkOption {
inherit (jsonFormat) type;
default = { };
example = {
ui.theme = "Default";
general = {
vimMode = true;
preferredEditor = "nvim";
previewFeatures = true;
};
ide.enabled = true;
privacy.usageStatisticsEnabled = false;
tools.autoAccept = false;
context.loadMemoryFromIncludeDirectories = true;
security.auth.selectedType = "oauth-personal";
};
description = "JSON config for gemini-cli";
};
commands =
let
commandType = lib.types.submodule {
freeformType = lib.types.str;
options = {
prompt = lib.mkOption {
type = lib.types.str;
description = ''
The prompt that will be sent to the Gemini model when the command is executed.
This can be a single-line or multi-line string.
The special placeholder {{args}} will be replaced with the text the user typed after the command name.
'';
};
description = lib.mkOption {
type = lib.types.str;
description = ''
A brief, one-line description of what the command does.
This text will be displayed next to your command in the /help menu.
If you omit this field, a generic description will be generated from the filename.
'';
};
};
};
in
lib.mkOption {
type = lib.types.attrsOf commandType;
default = { };
description = ''
An attribute set of custom commands that will be globally available.
The name of the attribute set will be the name of each command.
You may use subdirectories to create namespaced commands, such as `git/fix` becoming `/git:fix`.
See https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/commands.md#custom-commands for more information.
'';
example = lib.literalExpression ''
changelog = {
prompt =
'''
Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.
''';
description = "Adds a new entry to the project's CHANGELOG.md file.";
};
"git/fix" = { # Becomes /git:fix
prompt = "Please analyze the staged git changes and provide a code fix for the issue described here: {{args}}.";
description = "Generates a fix for a given GitHub issue.";
};
'';
};
policies = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.path tomlFormat.type);
default = { };
description = ''
An attribute set of policy definitions to create in `~/.gemini/policies/`.
The attribute name becomes the filename with `.toml` extension automatically added.
The value can be either an attribute set representing the TOML policy or a path to a TOML file.
'';
example = lib.literalExpression ''
{
"my-rules" = {
rule = [
{
toolName = "run_shell_command";
commandPrefix = "git ";
decision = "ask_user";
priority = 100;
}
];
};
"other-rules" = ./path/to/rules.toml;
}
'';
};
defaultModel = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "gemini-2.5-flash";
description = ''
The default model to use for the CLI.
Will be set as $GEMINI_MODEL when configured.
'';
};
context = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
default = { };
example = lib.literalExpression ''
{
GEMINI = '''
# Global Context
You are a helpful AI assistant for software development.
## Coding Standards
- Follow consistent code style
- Write clear comments
- Test your changes
''';
AGENTS = ./path/to/agents.md;
CONTEXT = '''
Additional context instructions here.
''';
}
'';
description = ''
Global context(s) for gemini-cli.
The attribute name becomes the filename, with a {file}`.md`
extension added automatically. Each value is either:
- Inline content as a string
- A path to a file containing the content
The configured files are written to {file}`~/.gemini/`.
Note: You can customize which context file names gemini-cli looks for by setting
`settings.context.fileName`. For example:
```nix
settings = {
context.fileName = ["AGENTS.md", "CONTEXT.md", "GEMINI.md"];
};
```
'';
};
skills = lib.mkOption {
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
default = { };
example = lib.literalExpression ''
{
xlsx = ./skills/xlsx/SKILL.md;
data-analysis = ./skills/data-analysis;
pdf-processing = '''
---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
---
# PDF Processing
## Quick start
Use pdfplumber to extract text from PDFs:
```python
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
text = pdf.pages[0].extract_text()
```
''';
}
'';
description = ''
Custom skills for gemini-cli.
This option can be either:
- An attribute set defining skills
- A path to a directory containing skill folders
If an attribute set is used, the attribute name becomes the
skill directory name, and the value is either:
- Inline content as a string (creates `~/.gemini/skills/<name>/SKILL.md`)
- A path to a file (creates `~/.gemini/skills/<name>/SKILL.md`)
- A path to a directory (symlinks `~/.gemini/skills/<name>/` to that directory)
If a path is used, it is expected to contain one folder per
skill name, each containing a {file}`SKILL.md`. The directory is
symlinked to {file}`~/.gemini/skills/`.
'';
};
};
config =
let
isStorePathString =
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
isPathLikeContent = content: lib.isPath content || isStorePathString content;
in
lib.mkIf cfg.enable (
lib.mkMerge [
{
programs.gemini-cli.settings.mcpServers = lib.mkIf (
cfg.enableMcpIntegration && config.programs.mcp.enable
) (lib.mapAttrs (_n: lib.mkDefault) config.programs.mcp.servers);
}
{
home = {
packages = lib.mkIf (cfg.package != null) [ cfg.package ];
file.".gemini/settings.json" = lib.mkIf (cfg.settings != { }) {
source = jsonFormat.generate "gemini-cli-settings.json" cfg.settings;
};
sessionVariables = lib.mkIf (cfg.defaultModel != null) {
GEMINI_MODEL = cfg.defaultModel;
};
};
}
{
home.file = lib.mapAttrs' (
n: v: lib.nameValuePair ".gemini/${n}.md" (if lib.isPath v then { source = v; } else { text = v; })
) cfg.context;
}
{
home.file = lib.mapAttrs' (
n: v:
lib.nameValuePair ".gemini/commands/${n}.toml" {
source = tomlFormat.generate "gemini-cli-command-${n}.toml" {
inherit (v) description prompt;
};
}
) cfg.commands;
}
{
home.file = lib.mapAttrs' (
n: v:
lib.nameValuePair ".gemini/policies/${n}.toml" {
source =
if builtins.isPath v || builtins.isString v || lib.isDerivation v then
v
else
tomlFormat.generate "gemini-cli-policy-${n}.toml" v;
}
) cfg.policies;
}
{
assertions = [
{
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
message = "`programs.gemini-cli.skills` must be a directory when set to a path";
}
];
}
{
home.file =
if isPathLikeContent cfg.skills then
{
".gemini/skills" = {
source = cfg.skills;
recursive = true;
};
}
else
lib.mapAttrs' (
n: v:
if isPathLikeContent v && lib.pathIsDirectory v then
lib.nameValuePair ".gemini/skills/${n}" {
source = v;
recursive = true;
}
else
lib.nameValuePair ".gemini/skills/${n}/SKILL.md" (
if isPathLikeContent v then { source = v; } else { text = v; }
)
) cfg.skills;
}
]
);
}