Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/tools/ask-gemini.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const askGeminiArgsSchema = z.object({
changeMode: z.boolean().default(false).describe("Enable structured change mode - formats prompts to prevent tool errors and returns structured edit suggestions that Claude can apply directly"),
chunkIndex: z.union([z.number(), z.string()]).optional().describe("Which chunk to return (1-based)"),
chunkCacheKey: z.string().optional().describe("Optional cache key for continuation"),
workingDirectory: z.string().optional().describe("Working directory to run Gemini from. Use drive root (e.g., 'C:/' or 'D:/') to access files on that drive."),
});

export const askGeminiTool: UnifiedTool = {
Expand All @@ -24,8 +25,8 @@ export const askGeminiTool: UnifiedTool = {
},
category: 'gemini',
execute: async (args, onProgress) => {
const { prompt, model, sandbox, changeMode, chunkIndex, chunkCacheKey } = args; if (!prompt?.trim()) { throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED); }
const { prompt, model, sandbox, changeMode, chunkIndex, chunkCacheKey, workingDirectory } = args; if (!prompt?.trim()) { throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED); }

if (changeMode && chunkIndex && chunkCacheKey) {
return processChangeModeOutput(
'', // empty for cache...
Expand All @@ -34,13 +35,14 @@ export const askGeminiTool: UnifiedTool = {
prompt as string
);
}

const result = await executeGeminiCLI(
prompt as string,
model as string | undefined,
!!sandbox,
!!changeMode,
onProgress
onProgress,
workingDirectory as string | undefined
);

if (changeMode) {
Expand Down
6 changes: 4 additions & 2 deletions src/utils/commandExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import { Logger } from "./logger.js";
export async function executeCommand(
command: string,
args: string[],
onProgress?: (newOutput: string) => void
onProgress?: (newOutput: string) => void,
cwd?: string
): Promise<string> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
Logger.commandExecution(command, args, startTime);

const childProcess = spawn(command, args, {
env: process.env,
shell: false,
shell: true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Enabling shell: true unconditionally can introduce security vulnerabilities if the command or arguments are not properly sanitized, especially on non-Windows platforms where it might not be necessary. Since this change is intended for Windows compatibility, it's safer to enable the shell only when running on Windows. This minimizes the potential attack surface of command injection.

Suggested change
shell: true,
shell: process.platform === "win32",

stdio: ["ignore", "pipe", "pipe"],
cwd: cwd,
});

let stdout = "";
Expand Down
31 changes: 12 additions & 19 deletions src/utils/geminiExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export async function executeGeminiCLI(
model?: string,
sandbox?: boolean,
changeMode?: boolean,
onProgress?: (newOutput: string) => void
onProgress?: (newOutput: string) => void,
cwd?: string
): Promise<string> {
let prompt_processed = prompt;

Expand Down Expand Up @@ -87,38 +88,30 @@ ${prompt_processed}
prompt_processed = changeModeInstructions;
}

const args = [];
const args = ['-y']; // YOLO mode for non-interactive
if (model) { args.push(CLI.FLAGS.MODEL, model); }
if (sandbox) { args.push(CLI.FLAGS.SANDBOX); }

// Ensure @ symbols work cross-platform by wrapping in quotes if needed
const finalPrompt = prompt_processed.includes('@') && !prompt_processed.startsWith('"')
? `"${prompt_processed}"`
: prompt_processed;

args.push(CLI.FLAGS.PROMPT, finalPrompt);

// Use positional prompt (not -p flag which is deprecated)
args.push(prompt_processed);
Comment on lines +91 to +96

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for constructing the arguments array is nearly identical here and in the catch block (lines 104-111). This duplication can lead to maintenance issues, where a change in one place might not be reflected in the other. To improve maintainability and reduce redundancy, consider extracting this logic into a single helper function.


try {
return await executeCommand(CLI.COMMANDS.GEMINI, args, onProgress);
return await executeCommand(CLI.COMMANDS.GEMINI, args, onProgress, cwd);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes(ERROR_MESSAGES.QUOTA_EXCEEDED) && model !== MODELS.FLASH) {
Logger.warn(`${ERROR_MESSAGES.QUOTA_EXCEEDED}. Falling back to ${MODELS.FLASH}.`);
await sendStatusMessage(STATUS_MESSAGES.FLASH_RETRY);
const fallbackArgs = [];
const fallbackArgs = ['-y']; // YOLO mode
fallbackArgs.push(CLI.FLAGS.MODEL, MODELS.FLASH);
if (sandbox) {
fallbackArgs.push(CLI.FLAGS.SANDBOX);
}

// Same @ symbol handling for fallback
const fallbackPrompt = prompt_processed.includes('@') && !prompt_processed.startsWith('"')
? `"${prompt_processed}"`
: prompt_processed;

fallbackArgs.push(CLI.FLAGS.PROMPT, fallbackPrompt);

// Use positional prompt
fallbackArgs.push(prompt_processed);
try {
const result = await executeCommand(CLI.COMMANDS.GEMINI, fallbackArgs, onProgress);
const result = await executeCommand(CLI.COMMANDS.GEMINI, fallbackArgs, onProgress, cwd);
Logger.warn(`Successfully executed with ${MODELS.FLASH} fallback.`);
await sendStatusMessage(STATUS_MESSAGES.FLASH_SUCCESS);
return result;
Expand Down