diff --git a/packages/cli/src/config/args.ts b/packages/cli/src/config/args.ts deleted file mode 100644 index a71b4b66..00000000 --- a/packages/cli/src/config/args.ts +++ /dev/null @@ -1,43 +0,0 @@ -import yargs from 'yargs/yargs'; -import { hideBin } from 'yargs/helpers'; - -const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash-preview-04-17'; - -export interface CliArgs { - target_dir: string | undefined; - model: string | undefined; - _: Array; // Captures positional arguments - // Add other expected args here if needed - // e.g., verbose?: boolean; -} - -export async function parseArguments(): Promise { - const argv = await yargs(hideBin(process.argv)) - .option('target_dir', { - alias: 'd', - type: 'string', - description: - 'The target directory for Gemini operations. Defaults to the current working directory.', - }) - .option('model', { - alias: 'm', - type: 'string', - description: `The Gemini model to use. Defaults to ${DEFAULT_GEMINI_MODEL}.`, - default: DEFAULT_GEMINI_MODEL, - }) - .help() - .alias('h', 'help') - .strict() // Keep strict mode to error on unknown options - .parseAsync(); - - // Handle warnings for extra arguments here - if (argv._ && argv._.length > 0) { - console.warn( - `Warning: Additional arguments provided (${argv._.join(', ')}), but will be ignored.`, - ); - } - - // Cast to the interface to ensure the structure aligns with expectations - // Use `unknown` first for safer casting if types might not perfectly match - return argv as unknown as CliArgs; -} diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts new file mode 100644 index 00000000..ca9cbc18 --- /dev/null +++ b/packages/cli/src/config/config.ts @@ -0,0 +1,110 @@ +import yargs from 'yargs/yargs'; +import { hideBin } from 'yargs/helpers'; +import * as dotenv from 'dotenv'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import process from 'node:process'; + +const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash-preview-04-17'; + +export class Config { + private apiKey: string; + private model: string; + private targetDir: string; + private extraArgs: (string | number)[]; // Captures positional arguments + + constructor(apiKey: string, model: string, targetDir: string, extraArgs: (string | number)[]) { + this.apiKey = apiKey; + this.model = model; + this.targetDir = targetDir; + this.extraArgs = extraArgs; + } + + getApiKey(): string { + return this.apiKey; + } + + getModel(): string { + return this.model; + } + + getTargetDir(): string { + return this.targetDir; + } + + getExtraArgs(): (string | number)[] { + return this.extraArgs; + } +} + +export function loadConfig(): Config { + loadEnvironment(); + const argv = parseArguments(); + return new Config( + process.env.GEMINI_API_KEY || "", + argv.model || process.env.GEMINI_API_KEY || DEFAULT_GEMINI_MODEL, + argv.target_dir || process.cwd(), + argv._, + ); +} + +export const globalConfig = loadConfig(); // TODO(jbd): Remove global state. + +interface CliArgs { + target_dir: string | undefined; + model: string | undefined; + _: (string | number)[]; // Captures positional arguments + // Add other expected args here if needed + // e.g., verbose?: boolean; +} + +function parseArguments(): CliArgs { + const argv = yargs(hideBin(process.argv)) + .option('target_dir', { + alias: 'd', + type: 'string', + description: + 'The target directory for Gemini operations. Defaults to the current working directory.', + }) + .option('model', { + alias: 'm', + type: 'string', + description: `The Gemini model to use. Defaults to ${DEFAULT_GEMINI_MODEL}.`, + default: DEFAULT_GEMINI_MODEL, + }) + .help() + .alias('h', 'help') + .strict() // Keep strict mode to error on unknown options + .argv; + + // Cast to the interface to ensure the structure aligns with expectations + // Use `unknown` first for safer casting if types might not perfectly match + return argv as unknown as CliArgs; +} + + +function findEnvFile(startDir: string): string | null { + // Start search from the provided directory (e.g., current working directory) + let currentDir = path.resolve(startDir); // Ensure absolute path + while (true) { + const envPath = path.join(currentDir, '.env'); + if (fs.existsSync(envPath)) { + return envPath; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir || !parentDir) { + return null; + } + currentDir = parentDir; + } +} + +function loadEnvironment(): void { + // Start searching from the current working directory by default + const envFilePath = findEnvFile(process.cwd()); + if (!envFilePath) { + return; + } + dotenv.config({ path: envFilePath }); +} diff --git a/packages/cli/src/config/env.ts b/packages/cli/src/config/env.ts deleted file mode 100644 index 51fc0a9c..00000000 --- a/packages/cli/src/config/env.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as dotenv from 'dotenv'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import process from 'node:process'; - -function findEnvFile(startDir: string): string | null { - // Start search from the provided directory (e.g., current working directory) - let currentDir = path.resolve(startDir); // Ensure absolute path - while (true) { - const envPath = path.join(currentDir, '.env'); - if (fs.existsSync(envPath)) { - return envPath; - } - - const parentDir = path.dirname(currentDir); - if (parentDir === currentDir || !parentDir) { - return null; - } - currentDir = parentDir; - } -} - -export function loadEnvironment(): void { - // Start searching from the current working directory by default - const envFilePath = findEnvFile(process.cwd()); - - if (envFilePath) { - dotenv.config({ path: envFilePath }); - } - - if (!process.env.GEMINI_API_KEY?.length) { - console.error( - 'Error: GEMINI_API_KEY environment variable is not set. Please visit https://ai.google.dev/gemini-api/docs/api-key to set up a new one.', - ); - process.exit(0); - } -} - -export function getApiKey(): string { - loadEnvironment(); - const apiKey = process.env.GEMINI_API_KEY; - if (!apiKey) { - throw new Error( - 'GEMINI_API_KEY environment variable is not set. Please visit https://ai.google.dev/gemini-api/docs/api-key to set up a new one.', - ); - } - return apiKey; -} diff --git a/packages/cli/src/config/globalConfig.ts b/packages/cli/src/config/globalConfig.ts deleted file mode 100644 index 2b6ad518..00000000 --- a/packages/cli/src/config/globalConfig.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CliArgs } from './args.js'; // Assuming CliArgs contains the needed fields - -interface GlobalConfig { - model: string; - // Add other global config values here if needed - // e.g., targetDir?: string; -} - -let config: GlobalConfig | null = null; - -/** - * Initializes the global configuration. Should only be called once at application startup. - * @param args The parsed command-line arguments. - */ -export function initializeConfig(args: Pick): void { - if (config) { - console.warn('Global configuration already initialized.'); - return; - } - if (!args.model) { - // This shouldn't happen if default is set correctly in args.ts - throw new Error('Model not provided during config initialization.'); - } - config = { - model: args.model, - // Initialize other config values from args here - }; -} - -/** - * Retrieves the globally stored configuration. - * Throws an error if the configuration has not been initialized. - * @returns The global configuration object. - */ -export function getConfig(): GlobalConfig { - if (!config) { - throw new Error( - 'Global configuration accessed before initialization. Call initializeConfig() first.', - ); - } - return config; -} - -/** - * Helper function to get the configured Gemini model name. - * @returns The model name string. - */ -export function getModel(): string { - return getConfig().model; -} \ No newline at end of file diff --git a/packages/cli/src/core/gemini-client.ts b/packages/cli/src/core/gemini-client.ts index 21cc7188..64bf87a3 100644 --- a/packages/cli/src/core/gemini-client.ts +++ b/packages/cli/src/core/gemini-client.ts @@ -8,8 +8,6 @@ import { PartListUnion, Content, } from '@google/genai'; -import { getApiKey } from '../config/env.js'; -import { getModel } from '../config/globalConfig.js'; import { CoreSystemPrompt } from './prompts.js'; import { type ToolCallEvent, @@ -21,6 +19,8 @@ import { toolRegistry } from '../tools/tool-registry.js'; import { ToolResult } from '../tools/tools.js'; import { getFolderStructure } from '../utils/getFolderStructure.js'; import { GeminiEventType, GeminiStream } from './gemini-stream.js'; +import { Config } from '../config/config.js'; + type ToolExecutionOutcome = { callId: string; @@ -32,6 +32,7 @@ type ToolExecutionOutcome = { }; export class GeminiClient { + private config: Config; private ai: GoogleGenAI; private defaultHyperParameters: GenerateContentConfig = { temperature: 0, @@ -39,14 +40,14 @@ export class GeminiClient { }; private readonly MAX_TURNS = 100; - constructor() { - const apiKey = getApiKey(); - this.ai = new GoogleGenAI({ apiKey }); + constructor(config: Config) { + this.config = config; + this.ai = new GoogleGenAI({ apiKey: config.getApiKey() }); } async startChat(): Promise { const tools = toolRegistry.getToolSchemas(); - const model = getModel(); + const model = this.config.getModel(); // --- Get environmental information --- const cwd = process.cwd(); @@ -446,7 +447,7 @@ Respond *only* in JSON format according to the following schema. Do not include contents: Content[], schema: SchemaUnion, ): Promise> { - const model = getModel(); + const model = this.config.getModel(); try { const result = await this.ai.models.generateContent({ model, diff --git a/packages/cli/src/gemini.ts b/packages/cli/src/gemini.ts index a2797dc8..8762d39b 100644 --- a/packages/cli/src/gemini.ts +++ b/packages/cli/src/gemini.ts @@ -1,10 +1,6 @@ import React from 'react'; import { render } from 'ink'; import App from './ui/App.js'; -import { parseArguments } from './config/args.js'; -import { loadEnvironment } from './config/env.js'; -import { initializeConfig } from './config/globalConfig.js'; -import { getTargetDirectory } from './utils/paths.js'; import { toolRegistry } from './tools/tool-registry.js'; import { LSTool } from './tools/ls.tool.js'; import { ReadFileTool } from './tools/read-file.tool.js'; @@ -13,21 +9,16 @@ import { GlobTool } from './tools/glob.tool.js'; import { EditTool } from './tools/edit.tool.js'; import { TerminalTool } from './tools/terminal.tool.js'; import { WriteFileTool } from './tools/write-file.tool.js'; +import { globalConfig } from './config/config.js'; async function main() { - // 1. Configuration - loadEnvironment(); - const argv = await parseArguments(); - initializeConfig({ model: argv.model as string }); - const targetDir = getTargetDirectory(argv.target_dir); + // Configure tools + registerTools(globalConfig.getTargetDir()); - // 2. Configure tools - registerTools(targetDir); - - // 3. Render UI + // Render UI render( React.createElement(App, { - directory: targetDir, + directory: globalConfig.getTargetDir(), }), ); } diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index f79aeaa3..1102e75d 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { Box, Text } from 'ink'; import TextInput from 'ink-text-input'; -import { getModel } from '../../config/globalConfig.js'; +import { globalConfig } from '../../config/config.js'; + + interface InputPromptProps { query: string; @@ -15,7 +17,7 @@ const InputPrompt: React.FC = ({ setQuery, onSubmit, }) => { - const model = getModel(); + const model = globalConfig.getModel(); return ( diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 63f110b5..0b62a40b 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -4,6 +4,7 @@ import { GeminiClient } from '../../core/gemini-client.js'; import { type Chat, type PartListUnion } from '@google/genai'; import { HistoryItem } from '../types.js'; import { processGeminiStream , StreamingState } from '../../core/gemini-stream.js'; +import { globalConfig } from '../../config/config.js'; const addHistoryItem = ( setHistory: React.Dispatch>, @@ -34,7 +35,7 @@ export const useGeminiStream = ( setInitError(null); if (!geminiClientRef.current) { try { - geminiClientRef.current = new GeminiClient(); + geminiClientRef.current = new GeminiClient(globalConfig); } catch (error: any) { setInitError( `Failed to initialize client: ${error.message || 'Unknown error'}`, diff --git a/packages/cli/src/utils/BackgroundTerminalAnalyzer.ts b/packages/cli/src/utils/BackgroundTerminalAnalyzer.ts index 6028f9b1..c73b1655 100644 --- a/packages/cli/src/utils/BackgroundTerminalAnalyzer.ts +++ b/packages/cli/src/utils/BackgroundTerminalAnalyzer.ts @@ -3,6 +3,7 @@ import { SchemaUnion, Type } from '@google/genai'; // Assuming these types exist import { GeminiClient } from '../core/gemini-client.js'; // Assuming this path import { exec } from 'child_process'; // Needed for Windows process check import { promisify } from 'util'; // To promisify exec +import { globalConfig } from '../config/config.js'; // Promisify child_process.exec for easier async/await usage const execAsync = promisify(exec); @@ -60,7 +61,7 @@ export class BackgroundTerminalAnalyzer { initialDelayMs?: number; } = {}, // Provide default options ) { - this.ai = aiClient || new GeminiClient(); // Call constructor without model + this.ai = aiClient || new GeminiClient(globalConfig); // Call constructor without model this.pollIntervalMs = options.pollIntervalMs ?? 5000; // Default 5 seconds this.maxAttempts = options.maxAttempts ?? 6; // Default 6 attempts (approx 30s total) this.initialDelayMs = options.initialDelayMs ?? 500; // Default 0.5s initial delay diff --git a/packages/cli/src/utils/paths.ts b/packages/cli/src/utils/paths.ts index 60762c9d..4dc6e5cb 100644 --- a/packages/cli/src/utils/paths.ts +++ b/packages/cli/src/utils/paths.ts @@ -1,13 +1,5 @@ -import process from 'node:process'; import path from 'node:path'; // Import the 'path' module -/** - * Returns the target directory, using the provided argument or the current working directory. - */ -export function getTargetDirectory(targetDirArg: string | undefined): string { - return targetDirArg || process.cwd(); -} - /** * Shortens a path string if it exceeds maxLen, prioritizing the start and end segments. * Example: /path/to/a/very/long/file.txt -> /path/.../long/file.txt