Introduce a config module to manage configuration (#22)
* Introduce a config module to manage configuration * Remove public modifier
This commit is contained in:
parent
e1fac40256
commit
3afaa8033b
|
@ -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;
|
|
||||||
}
|
|
|
@ -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 });
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -8,8 +8,6 @@ import {
|
||||||
PartListUnion,
|
PartListUnion,
|
||||||
Content,
|
Content,
|
||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
import { getApiKey } from '../config/env.js';
|
|
||||||
import { getModel } from '../config/globalConfig.js';
|
|
||||||
import { CoreSystemPrompt } from './prompts.js';
|
import { CoreSystemPrompt } from './prompts.js';
|
||||||
import {
|
import {
|
||||||
type ToolCallEvent,
|
type ToolCallEvent,
|
||||||
|
@ -21,6 +19,8 @@ import { toolRegistry } from '../tools/tool-registry.js';
|
||||||
import { ToolResult } from '../tools/tools.js';
|
import { ToolResult } from '../tools/tools.js';
|
||||||
import { getFolderStructure } from '../utils/getFolderStructure.js';
|
import { getFolderStructure } from '../utils/getFolderStructure.js';
|
||||||
import { GeminiEventType, GeminiStream } from './gemini-stream.js';
|
import { GeminiEventType, GeminiStream } from './gemini-stream.js';
|
||||||
|
import { Config } from '../config/config.js';
|
||||||
|
|
||||||
|
|
||||||
type ToolExecutionOutcome = {
|
type ToolExecutionOutcome = {
|
||||||
callId: string;
|
callId: string;
|
||||||
|
@ -32,6 +32,7 @@ type ToolExecutionOutcome = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class GeminiClient {
|
export class GeminiClient {
|
||||||
|
private config: Config;
|
||||||
private ai: GoogleGenAI;
|
private ai: GoogleGenAI;
|
||||||
private defaultHyperParameters: GenerateContentConfig = {
|
private defaultHyperParameters: GenerateContentConfig = {
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
|
@ -39,14 +40,14 @@ export class GeminiClient {
|
||||||
};
|
};
|
||||||
private readonly MAX_TURNS = 100;
|
private readonly MAX_TURNS = 100;
|
||||||
|
|
||||||
constructor() {
|
constructor(config: Config) {
|
||||||
const apiKey = getApiKey();
|
this.config = config;
|
||||||
this.ai = new GoogleGenAI({ apiKey });
|
this.ai = new GoogleGenAI({ apiKey: config.getApiKey() });
|
||||||
}
|
}
|
||||||
|
|
||||||
async startChat(): Promise<Chat> {
|
async startChat(): Promise<Chat> {
|
||||||
const tools = toolRegistry.getToolSchemas();
|
const tools = toolRegistry.getToolSchemas();
|
||||||
const model = getModel();
|
const model = this.config.getModel();
|
||||||
|
|
||||||
// --- Get environmental information ---
|
// --- Get environmental information ---
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
@ -446,7 +447,7 @@ Respond *only* in JSON format according to the following schema. Do not include
|
||||||
contents: Content[],
|
contents: Content[],
|
||||||
schema: SchemaUnion,
|
schema: SchemaUnion,
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
const model = getModel();
|
const model = this.config.getModel();
|
||||||
try {
|
try {
|
||||||
const result = await this.ai.models.generateContent({
|
const result = await this.ai.models.generateContent({
|
||||||
model,
|
model,
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'ink';
|
import { render } from 'ink';
|
||||||
import App from './ui/App.js';
|
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 { toolRegistry } from './tools/tool-registry.js';
|
||||||
import { LSTool } from './tools/ls.tool.js';
|
import { LSTool } from './tools/ls.tool.js';
|
||||||
import { ReadFileTool } from './tools/read-file.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 { EditTool } from './tools/edit.tool.js';
|
||||||
import { TerminalTool } from './tools/terminal.tool.js';
|
import { TerminalTool } from './tools/terminal.tool.js';
|
||||||
import { WriteFileTool } from './tools/write-file.tool.js';
|
import { WriteFileTool } from './tools/write-file.tool.js';
|
||||||
|
import { globalConfig } from './config/config.js';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// 1. Configuration
|
// Configure tools
|
||||||
loadEnvironment();
|
registerTools(globalConfig.getTargetDir());
|
||||||
const argv = await parseArguments();
|
|
||||||
initializeConfig({ model: argv.model as string });
|
|
||||||
const targetDir = getTargetDirectory(argv.target_dir);
|
|
||||||
|
|
||||||
// 2. Configure tools
|
// Render UI
|
||||||
registerTools(targetDir);
|
|
||||||
|
|
||||||
// 3. Render UI
|
|
||||||
render(
|
render(
|
||||||
React.createElement(App, {
|
React.createElement(App, {
|
||||||
directory: targetDir,
|
directory: globalConfig.getTargetDir(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import TextInput from 'ink-text-input';
|
import TextInput from 'ink-text-input';
|
||||||
import { getModel } from '../../config/globalConfig.js';
|
import { globalConfig } from '../../config/config.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface InputPromptProps {
|
interface InputPromptProps {
|
||||||
query: string;
|
query: string;
|
||||||
|
@ -15,7 +17,7 @@ const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
setQuery,
|
setQuery,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const model = getModel();
|
const model = globalConfig.getModel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box marginTop={1} borderStyle="round" borderColor={'white'} paddingX={1}>
|
<Box marginTop={1} borderStyle="round" borderColor={'white'} paddingX={1}>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { GeminiClient } from '../../core/gemini-client.js';
|
||||||
import { type Chat, type PartListUnion } from '@google/genai';
|
import { type Chat, type PartListUnion } from '@google/genai';
|
||||||
import { HistoryItem } from '../types.js';
|
import { HistoryItem } from '../types.js';
|
||||||
import { processGeminiStream , StreamingState } from '../../core/gemini-stream.js';
|
import { processGeminiStream , StreamingState } from '../../core/gemini-stream.js';
|
||||||
|
import { globalConfig } from '../../config/config.js';
|
||||||
|
|
||||||
const addHistoryItem = (
|
const addHistoryItem = (
|
||||||
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
||||||
|
@ -34,7 +35,7 @@ export const useGeminiStream = (
|
||||||
setInitError(null);
|
setInitError(null);
|
||||||
if (!geminiClientRef.current) {
|
if (!geminiClientRef.current) {
|
||||||
try {
|
try {
|
||||||
geminiClientRef.current = new GeminiClient();
|
geminiClientRef.current = new GeminiClient(globalConfig);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setInitError(
|
setInitError(
|
||||||
`Failed to initialize client: ${error.message || 'Unknown error'}`,
|
`Failed to initialize client: ${error.message || 'Unknown error'}`,
|
||||||
|
|
|
@ -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 { GeminiClient } from '../core/gemini-client.js'; // Assuming this path
|
||||||
import { exec } from 'child_process'; // Needed for Windows process check
|
import { exec } from 'child_process'; // Needed for Windows process check
|
||||||
import { promisify } from 'util'; // To promisify exec
|
import { promisify } from 'util'; // To promisify exec
|
||||||
|
import { globalConfig } from '../config/config.js';
|
||||||
|
|
||||||
// Promisify child_process.exec for easier async/await usage
|
// Promisify child_process.exec for easier async/await usage
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
@ -60,7 +61,7 @@ export class BackgroundTerminalAnalyzer {
|
||||||
initialDelayMs?: number;
|
initialDelayMs?: number;
|
||||||
} = {}, // Provide default options
|
} = {}, // 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.pollIntervalMs = options.pollIntervalMs ?? 5000; // Default 5 seconds
|
||||||
this.maxAttempts = options.maxAttempts ?? 6; // Default 6 attempts (approx 30s total)
|
this.maxAttempts = options.maxAttempts ?? 6; // Default 6 attempts (approx 30s total)
|
||||||
this.initialDelayMs = options.initialDelayMs ?? 500; // Default 0.5s initial delay
|
this.initialDelayMs = options.initialDelayMs ?? 500; // Default 0.5s initial delay
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
import process from 'node:process';
|
|
||||||
import path from 'node:path'; // Import the 'path' module
|
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.
|
* 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
|
* Example: /path/to/a/very/long/file.txt -> /path/.../long/file.txt
|
||||||
|
|
Loading…
Reference in New Issue