Skip to content

Commit 6642308

Browse files
authored
Merge branch 'google-gemini:main' into main
2 parents 07c79b1 + e9e2f55 commit 6642308

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1305
-865
lines changed

docs/cli/commands.md

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,67 @@ Your command definition files must be written in the TOML format and use the `.t
129129

130130
- `description` (String): 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.**
131131

132+
#### Handling Arguments
133+
134+
Custom commands support two powerful, low-friction methods for handling arguments. The CLI automatically chooses the correct method based on the content of your command's `prompt`.
135+
136+
##### 1. Shorthand Injection with `{{args}}`
137+
138+
If your `prompt` contains the special placeholder `{{args}}`, the CLI will replace that exact placeholder with all the text the user typed after the command name. This is perfect for simple, deterministic commands where you need to inject user input into a specific place in a larger prompt template.
139+
140+
**Example (`git/fix.toml`):**
141+
142+
```toml
143+
# In: ~/.gemini/commands/git/fix.toml
144+
# Invoked via: /git:fix "Button is misaligned on mobile"
145+
146+
description = "Generates a fix for a given GitHub issue."
147+
prompt = "Please analyze the staged git changes and provide a code fix for the issue described here: {{args}}."
148+
```
149+
150+
The model will receive the final prompt: `Please analyze the staged git changes and provide a code fix for the issue described here: "Button is misaligned on mobile".`
151+
152+
##### 2. Default Argument Handling
153+
154+
If your `prompt` does **not** contain the special placeholder `{{args}}`, the CLI uses a default behavior for handling arguments.
155+
156+
If you provide arguments to the command (e.g., `/mycommand arg1`), the CLI will append the full command you typed to the end of the prompt, separated by two newlines. This allows the model to see both the original instructions and the specific arguments you just provided.
157+
158+
If you do **not** provide any arguments (e.g., `/mycommand`), the prompt is sent to the model exactly as it is, with nothing appended.
159+
160+
**Example (`changelog.toml`):**
161+
162+
This example shows how to create a robust command by defining a role for the model, explaining where to find the user's input, and specifying the expected format and behavior.
163+
164+
```toml
165+
# In: <project>/.gemini/commands/changelog.toml
166+
# Invoked via: /changelog 1.2.0 added "Support for default argument parsing."
167+
168+
description = "Adds a new entry to the project's CHANGELOG.md file."
169+
prompt = """
170+
# Task: Update Changelog
171+
172+
You are an expert maintainer of this software project. A user has invoked a command to add a new entry to the changelog.
173+
174+
**The user's raw command is appended below your instructions.**
175+
176+
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.
177+
178+
## Expected Format
179+
The command follows this format: `/changelog <version> <type> <message>`
180+
- `<type>` must be one of: "added", "changed", "fixed", "removed".
181+
182+
## Behavior
183+
1. Read the `CHANGELOG.md` file.
184+
2. Find the section for the specified `<version>`.
185+
3. Add the `<message>` under the correct `<type>` heading.
186+
4. If the version or type section doesn't exist, create it.
187+
5. Adhere strictly to the "Keep a Changelog" format.
188+
"""
189+
```
190+
191+
When you run `/changelog 1.2.0 added "New feature"`, the final text sent to the model will be the original prompt followed by two newlines and the command you typed.
192+
132193
---
133194

134195
#### Example: A "Pure Function" Refactoring Command
@@ -175,11 +236,6 @@ That's it! You can now run your command in the CLI. First, you might add a file
175236

176237
Gemini CLI will then execute the multi-line prompt defined in your TOML file.
177238

178-
This initial version of custom commands is focused on static prompts. Future updates are planned to introduce more dynamic capabilities, including:
179-
180-
- **Argument Support:** Passing arguments from the command line directly into your `prompt` template.
181-
- **Shell Execution:** Creating commands that can run local shell scripts to gather context before running the prompt.
182-
183239
## At commands (`@`)
184240

185241
At commands are used to include the content of files or directories as part of your prompt to Gemini. These commands include git-aware filtering.

docs/cli/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ This example demonstrates how you can provide general project context, specific
429429
- Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory.
430430
- Scope: Provides context relevant to the entire project or a significant portion of it.
431431
3. **Sub-directory Context Files (Contextual/Local):**
432-
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.).
432+
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with a `memoryDiscoveryMaxDirs` field in your `settings.json` file.
433433
- Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project.
434434
- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt to the Gemini model. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
435435
- **Commands for Memory Management:**

docs/telemetry.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The following lists the precedence for applying telemetry settings, with items l
1919
- `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
2020
- `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
2121
- `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
22+
- `--telemetry-outfile <path>`: Redirects telemetry output to a file. See [Exporting to a file](#exporting-to-a-file).
2223

2324
1. **Environment variables:**
2425
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint`.
@@ -50,6 +51,16 @@ The following code can be added to your workspace (`.gemini/settings.json`) or u
5051
}
5152
```
5253

54+
### Exporting to a file
55+
56+
You can export all telemetry data to a file for local inspection.
57+
58+
To enable file export, use the `--telemetry-outfile` flag with a path to your desired output file. This must be run using `--telemetry-target=local`.
59+
60+
```bash
61+
gemini --telemetry --telemetry-target=local --telemetry-outfile=/path/to/telemetry.log "your prompt"
62+
```
63+
5364
## Running an OTEL Collector
5465

5566
An OTEL Collector is a service that receives, processes, and exports telemetry data.

packages/cli/src/config/config.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => {
3737
...actualServer,
3838
loadEnvironment: vi.fn(),
3939
loadServerHierarchicalMemory: vi.fn(
40-
(cwd, debug, fileService, extensionPaths) =>
40+
(cwd, debug, fileService, extensionPaths, _maxDirs) =>
4141
Promise.resolve({
4242
memoryContent: extensionPaths?.join(',') || '',
4343
fileCount: extensionPaths?.length || 0,
@@ -491,6 +491,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
491491
respectGitIgnore: false,
492492
respectGeminiIgnore: true,
493493
},
494+
undefined, // maxDirs
494495
);
495496
});
496497

packages/cli/src/config/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface CliArgs {
5555
telemetryTarget: string | undefined;
5656
telemetryOtlpEndpoint: string | undefined;
5757
telemetryLogPrompts: boolean | undefined;
58+
telemetryOutfile: string | undefined;
5859
allowedMcpServerNames: string[] | undefined;
5960
experimentalAcp: boolean | undefined;
6061
extensions: string[] | undefined;
@@ -159,6 +160,10 @@ export async function parseArguments(): Promise<CliArgs> {
159160
description:
160161
'Enable or disable logging of user prompts for telemetry. Overrides settings files.',
161162
})
163+
.option('telemetry-outfile', {
164+
type: 'string',
165+
description: 'Redirect all telemetry output to the specified file.',
166+
})
162167
.option('checkpointing', {
163168
alias: 'c',
164169
type: 'boolean',
@@ -220,6 +225,7 @@ export async function loadHierarchicalGeminiMemory(
220225
currentWorkingDirectory: string,
221226
debugMode: boolean,
222227
fileService: FileDiscoveryService,
228+
settings: Settings,
223229
extensionContextFilePaths: string[] = [],
224230
fileFilteringOptions?: FileFilteringOptions,
225231
): Promise<{ memoryContent: string; fileCount: number }> {
@@ -237,6 +243,7 @@ export async function loadHierarchicalGeminiMemory(
237243
fileService,
238244
extensionContextFilePaths,
239245
fileFilteringOptions,
246+
settings.memoryDiscoveryMaxDirs,
240247
);
241248
}
242249

@@ -293,6 +300,7 @@ export async function loadCliConfig(
293300
process.cwd(),
294301
debugMode,
295302
fileService,
303+
settings,
296304
extensionContextFilePaths,
297305
fileFiltering,
298306
);
@@ -412,6 +420,7 @@ export async function loadCliConfig(
412420
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
413421
settings.telemetry?.otlpEndpoint,
414422
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
423+
outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile,
415424
},
416425
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
417426
// Git-aware file filtering settings

packages/cli/src/config/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export interface Settings {
100100

101101
// Add other settings here.
102102
ideMode?: boolean;
103+
memoryDiscoveryMaxDirs?: number;
103104
}
104105

105106
export interface SettingsError {

packages/cli/src/services/FileCommandLoader.test.ts

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import mock from 'mock-fs';
1414
import { assert } from 'vitest';
1515
import { createMockCommandContext } from '../test-utils/mockCommandContext.js';
1616

17-
const mockContext = createMockCommandContext();
18-
1917
describe('FileCommandLoader', () => {
2018
const signal: AbortSignal = new AbortController().signal;
2119

@@ -39,7 +37,16 @@ describe('FileCommandLoader', () => {
3937
expect(command).toBeDefined();
4038
expect(command.name).toBe('test');
4139

42-
const result = await command.action?.(mockContext, '');
40+
const result = await command.action?.(
41+
createMockCommandContext({
42+
invocation: {
43+
raw: '/test',
44+
name: 'test',
45+
args: '',
46+
},
47+
}),
48+
'',
49+
);
4350
if (result?.type === 'submit_prompt') {
4451
expect(result.content).toBe('This is a test prompt');
4552
} else {
@@ -122,7 +129,16 @@ describe('FileCommandLoader', () => {
122129
const command = commands[0];
123130
expect(command).toBeDefined();
124131

125-
const result = await command.action?.(mockContext, '');
132+
const result = await command.action?.(
133+
createMockCommandContext({
134+
invocation: {
135+
raw: '/test',
136+
name: 'test',
137+
args: '',
138+
},
139+
}),
140+
'',
141+
);
126142
if (result?.type === 'submit_prompt') {
127143
expect(result.content).toBe('Project prompt');
128144
} else {
@@ -232,4 +248,70 @@ describe('FileCommandLoader', () => {
232248
// Verify that the ':' in the filename was replaced with an '_'
233249
expect(command.name).toBe('legacy_command');
234250
});
251+
252+
describe('Shorthand Argument Processor Integration', () => {
253+
it('correctly processes a command with {{args}}', async () => {
254+
const userCommandsDir = getUserCommandsDir();
255+
mock({
256+
[userCommandsDir]: {
257+
'shorthand.toml':
258+
'prompt = "The user wants to: {{args}}"\ndescription = "Shorthand test"',
259+
},
260+
});
261+
262+
const loader = new FileCommandLoader(null as unknown as Config);
263+
const commands = await loader.loadCommands(signal);
264+
const command = commands.find((c) => c.name === 'shorthand');
265+
expect(command).toBeDefined();
266+
267+
const result = await command!.action?.(
268+
createMockCommandContext({
269+
invocation: {
270+
raw: '/shorthand do something cool',
271+
name: 'shorthand',
272+
args: 'do something cool',
273+
},
274+
}),
275+
'do something cool',
276+
);
277+
expect(result?.type).toBe('submit_prompt');
278+
if (result?.type === 'submit_prompt') {
279+
expect(result.content).toBe('The user wants to: do something cool');
280+
}
281+
});
282+
});
283+
284+
describe('Default Argument Processor Integration', () => {
285+
it('correctly processes a command without {{args}}', async () => {
286+
const userCommandsDir = getUserCommandsDir();
287+
mock({
288+
[userCommandsDir]: {
289+
'model_led.toml':
290+
'prompt = "This is the instruction."\ndescription = "Default processor test"',
291+
},
292+
});
293+
294+
const loader = new FileCommandLoader(null as unknown as Config);
295+
const commands = await loader.loadCommands(signal);
296+
const command = commands.find((c) => c.name === 'model_led');
297+
expect(command).toBeDefined();
298+
299+
const result = await command!.action?.(
300+
createMockCommandContext({
301+
invocation: {
302+
raw: '/model_led 1.2.0 added "a feature"',
303+
name: 'model_led',
304+
args: '1.2.0 added "a feature"',
305+
},
306+
}),
307+
'1.2.0 added "a feature"',
308+
);
309+
expect(result?.type).toBe('submit_prompt');
310+
if (result?.type === 'submit_prompt') {
311+
const expectedContent =
312+
'This is the instruction.\n\n/model_led 1.2.0 added "a feature"';
313+
expect(result.content).toBe(expectedContent);
314+
}
315+
});
316+
});
235317
});

packages/cli/src/services/FileCommandLoader.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,20 @@ import {
1515
getUserCommandsDir,
1616
} from '@google/gemini-cli-core';
1717
import { ICommandLoader } from './types.js';
18-
import { CommandKind, SlashCommand } from '../ui/commands/types.js';
18+
import {
19+
CommandContext,
20+
CommandKind,
21+
SlashCommand,
22+
SubmitPromptActionReturn,
23+
} from '../ui/commands/types.js';
24+
import {
25+
DefaultArgumentProcessor,
26+
ShorthandArgumentProcessor,
27+
} from './prompt-processors/argumentProcessor.js';
28+
import {
29+
IPromptProcessor,
30+
SHORTHAND_ARGS_PLACEHOLDER,
31+
} from './prompt-processors/types.js';
1932

2033
/**
2134
* Defines the Zod schema for a command definition file. This serves as the
@@ -156,16 +169,45 @@ export class FileCommandLoader implements ICommandLoader {
156169
.map((segment) => segment.replaceAll(':', '_'))
157170
.join(':');
158171

172+
const processors: IPromptProcessor[] = [];
173+
174+
// The presence of '{{args}}' is the switch that determines the behavior.
175+
if (validDef.prompt.includes(SHORTHAND_ARGS_PLACEHOLDER)) {
176+
processors.push(new ShorthandArgumentProcessor());
177+
} else {
178+
processors.push(new DefaultArgumentProcessor());
179+
}
180+
159181
return {
160182
name: commandName,
161183
description:
162184
validDef.description ||
163185
`Custom command from ${path.basename(filePath)}`,
164186
kind: CommandKind.FILE,
165-
action: async () => ({
166-
type: 'submit_prompt',
167-
content: validDef.prompt,
168-
}),
187+
action: async (
188+
context: CommandContext,
189+
_args: string,
190+
): Promise<SubmitPromptActionReturn> => {
191+
if (!context.invocation) {
192+
console.error(
193+
`[FileCommandLoader] Critical error: Command '${commandName}' was executed without invocation context.`,
194+
);
195+
return {
196+
type: 'submit_prompt',
197+
content: validDef.prompt, // Fallback to unprocessed prompt
198+
};
199+
}
200+
201+
let processedPrompt = validDef.prompt;
202+
for (const processor of processors) {
203+
processedPrompt = await processor.process(processedPrompt, context);
204+
}
205+
206+
return {
207+
type: 'submit_prompt',
208+
content: processedPrompt,
209+
};
210+
},
169211
};
170212
}
171213
}

0 commit comments

Comments
 (0)