UNPKG

buroventures-harald-code

Version:

Harald Code - AI-powered coding assistant CLI

382 lines 16.9 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as fs from 'fs'; import * as path from 'path'; import { homedir } from 'node:os'; import yargs from 'yargs/yargs'; import { hideBin } from 'yargs/helpers'; import process from 'node:process'; import { Config, loadServerHierarchicalMemory, setGeminiMdFilename as setServerGeminiMdFilename, getCurrentGeminiMdFilename, ApprovalMode, DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL, DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, FileDiscoveryService, IdeClient, } from 'buroventures-harald-code-core'; import { annotateActiveExtensions } from './extension.js'; import { getCliVersion } from '../utils/version.js'; import { loadSandboxConfig } from './sandboxConfig.js'; // Simple console logger for now - replace with actual logger if available const logger = { // eslint-disable-next-line @typescript-eslint/no-explicit-any debug: (...args) => console.debug('[DEBUG]', ...args), // eslint-disable-next-line @typescript-eslint/no-explicit-any warn: (...args) => console.warn('[WARN]', ...args), // eslint-disable-next-line @typescript-eslint/no-explicit-any error: (...args) => console.error('[ERROR]', ...args), }; export async function parseArguments() { const yargsInstance = yargs(hideBin(process.argv)) .scriptName('harald') .usage('$0 [options]', 'Harald Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode\nUsage: harald [options] or harald-code [options]') .option('model', { alias: 'm', type: 'string', description: `Model`, default: process.env.GEMINI_MODEL, }) .option('prompt', { alias: 'p', type: 'string', description: 'Prompt. Appended to input on stdin (if any).', }) .option('prompt-interactive', { alias: 'i', type: 'string', description: 'Execute the provided prompt and continue in interactive mode', }) .option('sandbox', { alias: 's', type: 'boolean', description: 'Run in sandbox?', }) .option('sandbox-image', { type: 'string', description: 'Sandbox image URI.', }) .option('debug', { alias: 'd', type: 'boolean', description: 'Run in debug mode?', default: false, }) .option('all-files', { alias: ['a'], type: 'boolean', description: 'Include ALL files in context?', default: false, }) .option('all_files', { type: 'boolean', description: 'Include ALL files in context?', default: false, }) .deprecateOption('all_files', 'Use --all-files instead. We will be removing --all_files in the coming weeks.') .option('show-memory-usage', { type: 'boolean', description: 'Show memory usage in status bar', default: false, }) .option('show_memory_usage', { type: 'boolean', description: 'Show memory usage in status bar', default: false, }) .deprecateOption('show_memory_usage', 'Use --show-memory-usage instead. We will be removing --show_memory_usage in the coming weeks.') .option('yolo', { alias: 'y', type: 'boolean', description: 'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?', default: false, }) .option('telemetry', { type: 'boolean', description: 'Enable telemetry? This flag specifically controls if telemetry is sent. Other --telemetry-* flags set specific values but do not enable telemetry on their own.', }) .option('telemetry-target', { type: 'string', choices: ['local', 'gcp'], description: 'Set the telemetry target (local or gcp). Overrides settings files.', }) .option('telemetry-otlp-endpoint', { type: 'string', description: 'Set the OTLP endpoint for telemetry. Overrides environment variables and settings files.', }) .option('telemetry-log-prompts', { type: 'boolean', description: 'Enable or disable logging of user prompts for telemetry. Overrides settings files.', }) .option('telemetry-outfile', { type: 'string', description: 'Redirect all telemetry output to the specified file.', }) .option('checkpointing', { alias: 'c', type: 'boolean', description: 'Enables checkpointing of file edits', default: false, }) .option('experimental-acp', { type: 'boolean', description: 'Starts the agent in ACP mode', }) .option('allowed-mcp-server-names', { type: 'array', string: true, description: 'Allowed MCP server names', }) .option('extensions', { alias: 'e', type: 'array', string: true, description: 'A list of extensions to use. If not provided, all extensions are used.', }) .option('list-extensions', { alias: 'l', type: 'boolean', description: 'List all available extensions and exit.', }) .option('ide-mode-feature', { type: 'boolean', description: 'Run in IDE mode?', }) .option('openai-logging', { type: 'boolean', description: 'Enable logging of OpenAI API calls for debugging and analysis', }) .option('openai-api-key', { type: 'string', description: 'OpenAI API key to use for authentication', }) .option('openai-base-url', { type: 'string', description: 'OpenAI base URL (for custom endpoints)', }) .option('proxy', { type: 'string', description: 'Proxy for gemini client, like schema://user:password@host:port', }) .option('include-directories', { type: 'array', string: true, description: 'Additional directories to include in the workspace (comma-separated or multiple --include-directories)', coerce: (dirs) => // Handle comma-separated values dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())), }) .version(await getCliVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') .help() .alias('h', 'help') .strict() .check((argv) => { if (argv.prompt && argv.promptInteractive) { throw new Error('Cannot use both --prompt (-p) and --prompt-interactive (-i) together'); } return true; }); yargsInstance.wrap(yargsInstance.terminalWidth()); const result = yargsInstance.parseSync(); // The import format is now only controlled by settings.memoryImportFormat // We no longer accept it as a CLI argument return result; } // This function is now a thin wrapper around the server's implementation. // It's kept in the CLI for now as App.tsx directly calls it for memory refresh. // TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself. export async function loadHierarchicalGeminiMemory(currentWorkingDirectory, debugMode, fileService, settings, extensionContextFilePaths = [], memoryImportFormat = 'tree', fileFilteringOptions) { // FIX: Use real, canonical paths for a reliable comparison to handle symlinks. const realCwd = fs.realpathSync(path.resolve(currentWorkingDirectory)); const realHome = fs.realpathSync(path.resolve(homedir())); const isHomeDirectory = realCwd === realHome; // If it is the home directory, pass an empty string to the core memory // function to signal that it should skip the workspace search. const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory; if (debugMode) { logger.debug(`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`); } // Directly call the server function with the corrected path. return loadServerHierarchicalMemory(effectiveCwd, debugMode, fileService, extensionContextFilePaths, memoryImportFormat, fileFilteringOptions, settings.memoryDiscoveryMaxDirs); } export async function loadCliConfig(settings, extensions, sessionId, argv) { const debugMode = argv.debug || [process.env.DEBUG, process.env.DEBUG_MODE].some((v) => v === 'true' || v === '1') || false; const memoryImportFormat = settings.memoryImportFormat || 'tree'; const ideMode = settings.ideMode ?? false; const ideModeFeature = (argv.ideModeFeature ?? settings.ideModeFeature ?? false) && !process.env.SANDBOX; const ideClient = IdeClient.getInstance(ideMode && ideModeFeature); const allExtensions = annotateActiveExtensions(extensions, argv.extensions || []); const activeExtensions = extensions.filter((_, i) => allExtensions[i].isActive); // Handle OpenAI API key from command line if (argv.openaiApiKey) { process.env.OPENAI_API_KEY = argv.openaiApiKey; } // Handle OpenAI base URL from command line if (argv.openaiBaseUrl) { process.env.OPENAI_BASE_URL = argv.openaiBaseUrl; } // Set the context filename in the server's memoryTool module BEFORE loading memory // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed // directly to the Config constructor in core, and have core handle setGeminiMdFilename. // However, loadHierarchicalGeminiMemory is called *before* createServerConfig. if (settings.contextFileName) { setServerGeminiMdFilename(settings.contextFileName); } else { // Reset to default if not provided in settings. setServerGeminiMdFilename(getCurrentGeminiMdFilename()); } const extensionContextFilePaths = activeExtensions.flatMap((e) => e.contextFiles); const fileService = new FileDiscoveryService(process.cwd()); const fileFiltering = { ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, ...settings.fileFiltering, }; // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(process.cwd(), debugMode, fileService, settings, extensionContextFilePaths, memoryImportFormat, fileFiltering); let mcpServers = mergeMcpServers(settings, activeExtensions); const excludeTools = mergeExcludeTools(settings, activeExtensions); const blockedMcpServers = []; if (!argv.allowedMcpServerNames) { if (settings.allowMCPServers) { const allowedNames = new Set(settings.allowMCPServers.filter(Boolean)); if (allowedNames.size > 0) { mcpServers = Object.fromEntries(Object.entries(mcpServers).filter(([key]) => allowedNames.has(key))); } } if (settings.excludeMCPServers) { const excludedNames = new Set(settings.excludeMCPServers.filter(Boolean)); if (excludedNames.size > 0) { mcpServers = Object.fromEntries(Object.entries(mcpServers).filter(([key]) => !excludedNames.has(key))); } } } if (argv.allowedMcpServerNames) { const allowedNames = new Set(argv.allowedMcpServerNames.filter(Boolean)); if (allowedNames.size > 0) { mcpServers = Object.fromEntries(Object.entries(mcpServers).filter(([key, server]) => { const isAllowed = allowedNames.has(key); if (!isAllowed) { blockedMcpServers.push({ name: key, extensionName: server.extensionName || '', }); } return isAllowed; })); } else { blockedMcpServers.push(...Object.entries(mcpServers).map(([key, server]) => ({ name: key, extensionName: server.extensionName || '', }))); mcpServers = {}; } } const sandboxConfig = await loadSandboxConfig(settings, argv); return new Config({ sessionId, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, sandbox: sandboxConfig, targetDir: process.cwd(), includeDirectories: argv.includeDirectories, debugMode, question: argv.promptInteractive || argv.prompt || '', fullContext: argv.allFiles || argv.all_files || false, coreTools: settings.coreTools || undefined, excludeTools, toolDiscoveryCommand: settings.toolDiscoveryCommand, toolCallCommand: settings.toolCallCommand, mcpServerCommand: settings.mcpServerCommand, mcpServers, userMemory: memoryContent, geminiMdFileCount: fileCount, approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT, showMemoryUsage: argv.showMemoryUsage || argv.show_memory_usage || settings.showMemoryUsage || false, accessibility: settings.accessibility, telemetry: { enabled: argv.telemetry ?? settings.telemetry?.enabled, target: (argv.telemetryTarget ?? settings.telemetry?.target), otlpEndpoint: argv.telemetryOtlpEndpoint ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? settings.telemetry?.otlpEndpoint, logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts, outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile, }, usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true, // Git-aware file filtering settings fileFiltering: { respectGitIgnore: settings.fileFiltering?.respectGitIgnore, respectGeminiIgnore: settings.fileFiltering?.respectGeminiIgnore, enableRecursiveFileSearch: settings.fileFiltering?.enableRecursiveFileSearch, }, checkpointing: argv.checkpointing || settings.checkpointing?.enabled, proxy: argv.proxy || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy, cwd: process.cwd(), fileDiscoveryService: fileService, bugCommand: settings.bugCommand, model: argv.model || settings.model || DEFAULT_GEMINI_MODEL, extensionContextFilePaths, maxSessionTurns: settings.maxSessionTurns ?? -1, sessionTokenLimit: settings.sessionTokenLimit ?? 32000, maxFolderItems: settings.maxFolderItems ?? 20, experimentalAcp: argv.experimentalAcp || false, listExtensions: argv.listExtensions || false, extensions: allExtensions, blockedMcpServers, noBrowser: !!process.env.NO_BROWSER, summarizeToolOutput: settings.summarizeToolOutput, ideMode, ideModeFeature, ideClient, enableOpenAILogging: (typeof argv.openaiLogging === 'undefined' ? settings.enableOpenAILogging : argv.openaiLogging) ?? false, sampling_params: settings.sampling_params, systemPromptMappings: settings.systemPromptMappings ?? [ { baseUrls: [ 'https://dashscope.aliyuncs.com/compatible-mode/v1/', 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/', ], modelNames: ['qwen3-coder-plus'], template: 'SYSTEM_TEMPLATE:{"name":"qwen3_coder","params":{"is_git_repository":{RUNTIME_VARS_IS_GIT_REPO},"sandbox":"{RUNTIME_VARS_SANDBOX}"}}', }, ], contentGenerator: settings.contentGenerator, settings, // Pass settings for API key rotation and other features }); } function mergeMcpServers(settings, extensions) { const mcpServers = { ...(settings.mcpServers || {}) }; for (const extension of extensions) { Object.entries(extension.config.mcpServers || {}).forEach(([key, server]) => { if (mcpServers[key]) { logger.warn(`Skipping extension MCP config for server with key "${key}" as it already exists.`); return; } mcpServers[key] = { ...server, extensionName: extension.config.name, }; }); } return mcpServers; } function mergeExcludeTools(settings, extensions) { const allExcludeTools = new Set(settings.excludeTools || []); for (const extension of extensions) { for (const tool of extension.config.excludeTools || []) { allExcludeTools.add(tool); } } return [...allExcludeTools]; } //# sourceMappingURL=config.js.map