summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaana Dogan <[email protected]>2025-04-18 11:12:18 -0700
committerGitHub <[email protected]>2025-04-18 11:12:18 -0700
commit3afaa8033bdd9d6af99ff378cafcd84afd76c59e (patch)
treeeecba3f035a7147281dfe4db5ca015108ae9d3dc
parente1fac4025606246e284b3e370d22100e8a25d652 (diff)
Introduce a config module to manage configuration (#22)
* Introduce a config module to manage configuration * Remove public modifier
-rw-r--r--packages/cli/src/config/args.ts43
-rw-r--r--packages/cli/src/config/config.ts110
-rw-r--r--packages/cli/src/config/env.ts48
-rw-r--r--packages/cli/src/config/globalConfig.ts50
-rw-r--r--packages/cli/src/core/gemini-client.ts15
-rw-r--r--packages/cli/src/gemini.ts19
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx6
-rw-r--r--packages/cli/src/ui/hooks/useGeminiStream.ts3
-rw-r--r--packages/cli/src/utils/BackgroundTerminalAnalyzer.ts3
-rw-r--r--packages/cli/src/utils/paths.ts8
10 files changed, 131 insertions, 174 deletions
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<string | number>; // Captures positional arguments
- // Add other expected args here if needed
- // e.g., verbose?: boolean;
-}
-
-export async function parseArguments(): Promise<CliArgs> {
- 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<CliArgs, 'model'>): 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<Chat> {
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<Record<string, unknown>> {
- 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<InputPromptProps> = ({
setQuery,
onSubmit,
}) => {
- const model = getModel();
+ const model = globalConfig.getModel();
return (
<Box marginTop={1} borderStyle="round" borderColor={'white'} paddingX={1}>
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<React.SetStateAction<HistoryItem[]>>,
@@ -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,14 +1,6 @@
-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
*/