From 166ec7fe10b0a37c9ae0b74cbf8dea7fd6fcc4b6 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 30 Jun 2025 08:35:54 +0700 Subject: [PATCH] feat: update readme and contribuing guide --- .env.example | 12 + CONTRIBUTING.md | 248 ++++++++++++++++++ README.md | 248 +++++++++++++++++- src/app/cli/cli.ts | 20 +- src/app/cli/utils/options.ts | 4 +- src/app/index.ts | 27 +- src/core/brain/llm/messages/manager.ts | 12 +- .../brain/memAgent/__test__/agent.test.ts | 6 +- src/core/brain/memAgent/loader.ts | 3 +- src/core/brain/memAgent/state-manager.ts | 15 +- src/core/env.ts | 35 +-- src/core/logger/__test__/logger.test.ts | 10 +- src/core/logger/logger.ts | 16 +- src/core/mcp/__test__/manager.test.ts | 2 +- src/core/mcp/client.ts | 55 ++-- src/core/mcp/types.ts | 2 +- src/core/session/session-manager.ts | 2 +- 17 files changed, 626 insertions(+), 91 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/.env.example b/.env.example index e69de29bb..451ea6362 100644 --- a/.env.example +++ b/.env.example @@ -0,0 +1,12 @@ +# Cipher Memory Agent Environment Configuration +# Copy this file and fill in your actual API keys + +# OpenAI Configuration (optional - only if using OpenAI) +#OPENAI_API_KEY=your_openai_api_key_here + +# Anthropic Configuration (optional - only if using Anthropic) +# ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# Logger Configuration (optional) +# CIPHER_LOG_LEVEL=info +# REDACT_SECRETS=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0dcb63f3d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,248 @@ +# Contributing to Cipher + +Thank you for your interest in contributing to **cipher**! We're excited to have you as part of our community building the next generation of agent memory systems with Model Context Protocol (MCP). + +## ๐Ÿš€ Quick Start + +### Prerequisites + +Before you begin, ensure you have: + +- **Node.js** โ‰ฅ 20.0.0 +- **pnpm** โ‰ฅ 9.14.0 (we use pnpm, not npm) +- **Git** configured with your GitHub account + +### 1. Fork & Clone + +1. **Fork** the repository to your GitHub account by clicking the "Fork" button +2. **Clone** your fork: + + ```bash + git clone https://github.com/YOUR_USERNAME/cipher.git + cd cipher + ``` + +### 2. Set Up Development Environment + +```bash +# Install dependencies +pnpm install + +# Set up environment variables +cp .env.example .env +# Edit .env and add your API keys (at least one required): +# OPENAI_API_KEY=your_openai_key +# ANTHROPIC_API_KEY=your_anthropic_key + +# Build the project +pnpm run build + +# Verify setup by running tests +pnpm test +``` + +### 3. Create Feature Branch + +```bash +git checkout -b feature/your-descriptive-branch-name +# Examples: +# - feature/add-memory-persistence +# - fix/mcp-connection-timeout +# - docs/update-api-examples +``` + +## ๐Ÿ›  Development Workflow + +### Code Quality Standards + +Before committing, ensure your code meets our standards: + +```bash +# Type checking +pnpm run typecheck + +# Linting (with auto-fix) +pnpm run lint:fix + +# Code formatting +pnpm run format + +# Run tests +pnpm test + +# Full build verification +pnpm run build +``` + +### Project Structure + +Understanding the codebase structure: + +``` +src/ +โ”œโ”€โ”€ app/ # CLI application entry point +โ”œโ”€โ”€ core/ +โ”‚ โ”œโ”€โ”€ brain/ # Core agent logic +โ”‚ โ”‚ โ”œโ”€โ”€ llm/ # LLM providers (OpenAI, Anthropic) +โ”‚ โ”‚ โ”œโ”€โ”€ memAgent/ # Agent management +โ”‚ โ”‚ โ””โ”€โ”€ systemPrompt/ +โ”‚ โ”œโ”€โ”€ mcp/ # Model Context Protocol integration +โ”‚ โ”œโ”€โ”€ session/ # Session management +โ”‚ โ””โ”€โ”€ logger/ # Logging infrastructure +โ””โ”€โ”€ utils/ # Shared utilities +``` + +### Development Guidelines + +#### TypeScript Best Practices + +- Use strict TypeScript configuration +- Implement proper error handling with custom error types +- Use interfaces and types for clear API contracts + +#### Code Style + +- Follow existing naming conventions +- Write self-documenting code with clear variable names +- Add JSDoc comments for public APIs +- Maintain consistent indentation and formatting + +#### Testing + +- Write tests for new functionality in `__test__/` directories +- Maintain or improve test coverage +- Use descriptive test names that explain the behavior +- Mock external dependencies appropriately + +#### MCP Integration + +- Follow MCP protocol specifications +- Implement proper connection lifecycle management +- Add timeout and error handling for server connections +- Use type-safe server configuration validation + +## ๐Ÿ“‹ Contribution Types + +### ๐Ÿ› Bug Fixes + +- Check existing issues before creating new ones +- Include reproduction steps and environment details +- Write regression tests to prevent future occurrences + +### โœจ New Features + +- **Open an issue first** for discussion on larger features +- Ensure the feature aligns with cipher's core mission +- Include comprehensive tests and documentation +- Update configuration schemas if needed + +### ๐Ÿ“š Documentation + +- Keep README.md and docs/ up to date +- Include code examples that work out-of-the-box +- Update configuration references for new options + +### ๐Ÿ”ง Refactoring + +- Maintain backward compatibility unless discussed +- Include performance benchmarks for optimization claims +- Update related tests and documentation + +## ๐Ÿ”„ Submission Process + +### 5. Commit Your Changes + +Follow conventional commit format: + +```bash +git add . +git commit -m "feat: add persistent memory layer for agent sessions" +# Other examples: +# git commit -m "fix: resolve MCP connection timeout issues" +# git commit -m "docs: update configuration examples" +# git commit -m "test: add integration tests for memory persistence" +``` + +### 6. Push and Create Pull Request + +```bash +git push origin feature/your-branch-name +``` + +Open a Pull Request against the `main` branch with: + +- **Clear title** describing the change +- **Detailed description** explaining: + - What problem this solves + - How you solved it + - Any breaking changes + - Testing performed +- **Link related issues** using `Fixes #123` or `Closes #123` + +### Pull Request Checklist + +- [ ] Code follows project style guidelines +- [ ] Self-review completed +- [ ] Tests added/updated and passing +- [ ] Documentation updated if needed +- [ ] No breaking changes (or clearly documented) +- [ ] Commits follow conventional format +- [ ] Branch is up to date with main + +## ๐Ÿงช Testing + +### Running Tests + +```bash +# Run all tests +pnpm test + +``` + +### Test Categories + +- **Unit tests**: Test individual functions and classes + +## ๐Ÿ› Reporting Issues + +When reporting bugs, include: + +1. **Environment details**: Node.js version, OS, pnpm version +2. **Reproduction steps**: Minimal code example that demonstrates the issue +3. **Expected behavior**: What should happen +4. **Actual behavior**: What actually happens +5. **Configuration**: Relevant parts of your `cipher.yml` and `.env` (redact API keys) +6. **Logs**: Any error messages or relevant log output + +## ๐Ÿ’ก Feature Requests + +Before suggesting new features: + +1. **Check existing issues** to avoid duplicates +2. **Explain the use case** - what problem does this solve? +3. **Propose implementation** if you have ideas +4. **Consider alternatives** that might already exist + +## ๐Ÿท Release Process + +We follow semantic versioning: + +- **Patch** (0.1.x): Bug fixes, small improvements +- **Minor** (0.x.0): New features, non-breaking changes +- **Major** (x.0.0): Breaking changes + +## ๐Ÿค Community + +- **Discord**: Join our [Discord community](https://discord.com/invite/UMRrpNjh5W) +- **GitHub Discussions**: For broader conversations and Q&A +- **Issues**: For bug reports and feature requests + +## ๐Ÿ“œ License + +By contributing to cipher, you agree that your contributions will be licensed under the [Apache License 2.0](LICENSE). + +--- + +**Questions?** Don't hesitate to ask in our Discord or open a discussion on GitHub. We're here to help make your contribution experience smooth and rewarding! + +Happy coding! ๐ŸŽ‰ diff --git a/README.md b/README.md index 9ae9677ce..f191492d9 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,257 @@ We welcome all kinds of [contributions](/CONTRIBUTING.md), feedbacks, and sugges ## Get Started -### Quickstart +```bash +# build from source +pnpm i && pnpm run build && npm link +``` + +## Run Modes + +Cipher supports two operational modes to fit different usage patterns: + +### CLI Mode (Interactive) + +The default mode provides an interactive command-line interface for direct conversation with your memory-powered agent: + +```bash +# Run in interactive CLI mode (default) +cipher +# or explicitly specify CLI mode +cipher --mode cli +``` + +**Features:** + +- Real-time conversation with the agent +- Persistent memory throughout the session +- Memory learning from every interaction +- Graceful exit with `exit` or `quit` commands +- Signal handling (Ctrl+C) for clean shutdown + +### MCP Server Mode + +Runs cipher as a Model Context Protocol server, allowing other MCP-compatible tools to connect and utilize the agent's memory capabilities: + +```bash +# Run as MCP server +cipher --mode mcp +``` + +**Features:** + +- Exposes agent capabilities via MCP protocol +- Enables integration with other MCP-compatible tools +- Persistent memory across client connections +- *Note: This mode is currently in development* + +### Prerequisites + +Before running cipher in any mode, ensure you have: + +1. **Environment Configuration**: Copy `.env.example` to `.env` and configure at least one API provider: + + ```bash + cp .env.example .env + # Edit .env and add your API keys + ``` + +2. **API Keys**: Set at least one of these in your `.env` file: + - `OPENAI_API_KEY` for OpenAI models + - `ANTHROPIC_API_KEY` for Anthropic Claude models + +3. **Agent Configuration**: The agent uses `memAgent/cipher.yml` for configuration (included in the project) + +### Additional Options + +```bash +# Disable verbose output +cipher --no-verbose + +# Show version +cipher --version + +# Show help +cipher --help +``` + +## Configuration + +Cipher uses a YAML configuration file (`memAgent/cipher.yml`) and environment variables for setup. The configuration is validated using strict schemas to ensure reliability. + +### Configuration File Structure + +The main configuration file is located at `memAgent/cipher.yml` and follows this structure: + +```yaml +# LLM Configuration (Required) +llm: + provider: openai # Required: 'openai' or 'anthropic' + model: gpt-4.1-mini # Required: Model name for the provider + apiKey: $OPENAI_API_KEY # Required: API key (supports env vars with $VAR syntax) + maxIterations: 50 # Optional: Max iterations for agentic loops (default: 50) + baseURL: https://api.openai.com/v1 # Optional: Custom API base URL (OpenAI only) + +# System Prompt (Required) +systemPrompt: "You are a helpful AI assistant with memory capabilities." + +# MCP Servers Configuration (Optional) +mcpServers: + filesystem: # Server name (can be any identifier) + type: stdio # Connection type: 'stdio', 'sse', or 'http' + command: npx # Command to launch the server + args: # Arguments for the command + - -y + - "@modelcontextprotocol/server-filesystem" + - . + env: # Environment variables for the server + HOME: /Users/username + timeout: 30000 # Connection timeout in ms (default: 30000) + connectionMode: lenient # 'strict' or 'lenient' (default: lenient) + +# Session Management (Optional) +sessions: + maxSessions: 100 # Maximum concurrent sessions (default: 100) + sessionTTL: 3600000 # Session TTL in milliseconds (default: 1 hour) + +# Agent Card (Optional) - for MCP server mode +agentCard: + name: cipher # Agent name (default: cipher) + description: "Custom description" # Agent description + version: "1.0.0" # Version (default: 1.0.0) + provider: + organization: your-org # Organization name + url: https://your-site.com # Organization URL +``` + +### Environment Variables + +Create a `.env` file in the project root for sensitive configuration: + +```bash +# API Keys (at least one required) +OPENAI_API_KEY=your_openai_api_key_here +ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# API Configuration (optional) +OPENAI_BASE_URL=https://api.openai.com/v1 + +# Logger Configuration (optional) +CIPHER_LOG_LEVEL=info # debug, info, warn, error +REDACT_SECRETS=true # true/false - redact sensitive info in logs +``` + +### LLM Provider Configuration + +#### OpenAI + +```yaml +llm: + provider: openai + model: gpt-4.1 # or o4-mini, etc. + apiKey: $OPENAI_API_KEY + baseURL: https://api.openai.com/v1 # Optional: for custom endpoints +``` + +#### Anthropic Claude + +```yaml +llm: + provider: anthropic + model: claude-4-sonnet-20250514 # or claude-3-7-sonnet-20250219, etc. + apiKey: $ANTHROPIC_API_KEY +``` + +### MCP Server Types + +#### Stdio Servers (Local Processes) + +```yaml +mcpServers: + myserver: + type: stdio + command: node # or python, uvx, etc. + args: ["server.js", "--port=3000"] + env: + API_KEY: $MY_API_KEY + timeout: 30000 + connectionMode: lenient +``` + +#### SSE Servers (Server-Sent Events) + +```yaml +mcpServers: + sse_server: + type: sse + url: https://api.example.com/sse + headers: + Authorization: "Bearer $TOKEN" + timeout: 30000 + connectionMode: strict +``` + +#### HTTP Servers (REST APIs) + +```yaml +mcpServers: + http_server: + type: http + url: https://api.example.com + headers: + Authorization: "Bearer $TOKEN" + User-Agent: "Cipher/1.0" + timeout: 30000 + connectionMode: lenient +``` + +### Configuration Validation + +Cipher validates all configuration at startup: + +- **LLM Provider**: Must be 'openai' or 'anthropic' +- **API Keys**: Must be non-empty strings +- **URLs**: Must be valid URLs when provided +- **Numbers**: Must be positive integers where specified +- **MCP Server Types**: Must be 'stdio', 'sse', or 'http' + +### Environment Variable Expansion + +You can use environment variables anywhere in the YAML configuration: + +```yaml +llm: + apiKey: $OPENAI_API_KEY # Simple expansion + baseURL: ${API_BASE_URL} # Brace syntax + model: ${MODEL_NAME:-gpt-4} # With default value (syntax may vary) +``` + +### Configuration Loading + +1. Cipher looks for `memAgent/cipher.yml` in the current directory +2. Environment variables are loaded from `.env` if present +3. Configuration is parsed, validated, and environment variables are expanded + +## Capabilities + +**MCP integration**: cipher handles all the complexity of MCP connections +**Dual layers Memory**: cipher leverages two layers of memory: knowledge base && +refelection + +## LLM Providers + +Cipher currently supports multiple LLLM providers: + +- **OpenAI**: `gpt-4.1-mini`, `gpt-4.1`, `o4-mini`, `o3` +**Anthropic**: `claude-4-sonnet-20250514`, `claude-3-7-sonnet-20250219` ## Contributing +We welcome contributions! Refer to our [Contributing Guide](./CONTRIBUTING.md) for more details. + ## Community & Support -Join our [Discord](https://discord.gg/byterover) to chat with the community and get support. +Join our [Discord](https://discord.com/invite/UMRrpNjh5W) to chat with the community and get support. If you're enjoying this project, please give us a โญ on GitHub! diff --git a/src/app/cli/cli.ts b/src/app/cli/cli.ts index f448431cd..0231e3409 100644 --- a/src/app/cli/cli.ts +++ b/src/app/cli/cli.ts @@ -1,4 +1,4 @@ -import { MemAgent, logger } from "@core/index.js"; +import { MemAgent, logger } from '@core/index.js'; import * as readline from 'readline'; import chalk from 'chalk'; @@ -8,7 +8,7 @@ import chalk from 'chalk'; export async function startInteractiveCli(agent: MemAgent): Promise { // Common initialization await _initCli(agent); - + console.log(chalk.cyan('๐Ÿš€ Welcome to Cipher Interactive CLI!')); console.log(chalk.gray('Your memory-powered coding assistant is ready.')); console.log(chalk.gray('Type "exit" or "quit" to end the session.\n')); @@ -16,7 +16,7 @@ export async function startInteractiveCli(agent: MemAgent): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, - prompt: chalk.blue('cipher> ') + prompt: chalk.blue('cipher> '), }); // Set up graceful shutdown @@ -33,7 +33,7 @@ export async function startInteractiveCli(agent: MemAgent): Promise { rl.on('line', async (input: string) => { const trimmedInput = input.trim(); - + // Handle exit commands if (trimmedInput.toLowerCase() === 'exit' || trimmedInput.toLowerCase() === 'quit') { handleExit(); @@ -49,7 +49,7 @@ export async function startInteractiveCli(agent: MemAgent): Promise { try { console.log(chalk.gray('๐Ÿค” Thinking...')); const response = await agent.run(trimmedInput); - + if (response) { // Display the AI response with nice formatting logger.displayAIResponse(response); @@ -76,15 +76,15 @@ export async function startInteractiveCli(agent: MemAgent): Promise { */ export async function startMcpMode(agent: MemAgent): Promise { await _initCli(agent); - + console.log(chalk.cyan('๐Ÿ”— Starting Cipher in MCP Server Mode...')); console.log(chalk.gray('Ready to accept MCP client connections.')); - + // TODO: Implement MCP server functionality // This would start an MCP server that other tools can connect to logger.info('MCP mode is not yet fully implemented'); logger.info('This would start a server that accepts MCP client connections'); - + // Keep the process alive process.stdin.resume(); } @@ -94,11 +94,11 @@ export async function startMcpMode(agent: MemAgent): Promise { */ async function _initCli(agent: MemAgent): Promise { logger.info('Initializing CLI interface...'); - + // Ensure agent is started if (!agent) { throw new Error('Agent is not initialized'); } - + logger.info('CLI interface ready'); } diff --git a/src/app/cli/utils/options.ts b/src/app/cli/utils/options.ts index 2d14be3ff..6ed614944 100644 --- a/src/app/cli/utils/options.ts +++ b/src/app/cli/utils/options.ts @@ -19,11 +19,11 @@ export function validateCliOptions(opts: any): void { verbose: opts.verbose, mode: opts.mode, }); - + if (!result.success) { throw result.error; } - + logger.debug('CLI options validated successfully', result.data); } diff --git a/src/app/index.ts b/src/app/index.ts index 93fb52d10..2e8927c33 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -15,11 +15,7 @@ program .description('Agent that can help to remember your vibe coding agent knowledge and reinforce it') .version(pkg.version, '-v, --version', 'output the current version') .option('--no-verbose', 'Disable verbose output') - .option( - '--mode ', - 'The application mode for cipher memory agent - cli | mcp', - 'cli' - ); + .option('--mode ', 'The application mode for cipher memory agent - cli | mcp', 'cli'); program .description( @@ -33,16 +29,18 @@ program logger.error('No .env file found, copy .env.example to .env and fill in the values'); process.exit(1); } - + // Check if at least one API key is provided if (!env.OPENAI_API_KEY && !env.ANTHROPIC_API_KEY) { - logger.error('No API key found, please set at least one of OPENAI_API_KEY or ANTHROPIC_API_KEY in .env file'); + logger.error( + 'No API key found, please set at least one of OPENAI_API_KEY or ANTHROPIC_API_KEY in .env file' + ); logger.error('Available providers: OpenAI, Anthropic'); process.exit(1); } const opts = program.opts(); - + // validate cli options try { validateCliOptions(opts); @@ -55,21 +53,26 @@ program try { const configPath = DEFAULT_CONFIG_PATH; logger.info(`Loading agent config from ${configPath}`); - + // Check if config file exists if (!existsSync(configPath)) { logger.error(`Config file not found at ${configPath}`); - logger.error('Please ensure the config file exists or create one based on memAgent/cipher.yml'); + logger.error( + 'Please ensure the config file exists or create one based on memAgent/cipher.yml' + ); process.exit(1); } - + const cfg = await loadAgentConfig(configPath); agent = new MemAgent(cfg); // Start the agent (initialize async services) await agent.start(); } catch (err) { - logger.error('Failed to load agent config:', err instanceof Error ? err.message : String(err)); + logger.error( + 'Failed to load agent config:', + err instanceof Error ? err.message : String(err) + ); process.exit(1); } diff --git a/src/core/brain/llm/messages/manager.ts b/src/core/brain/llm/messages/manager.ts index c603a085d..d4a5abf13 100644 --- a/src/core/brain/llm/messages/manager.ts +++ b/src/core/brain/llm/messages/manager.ts @@ -182,15 +182,15 @@ export class ContextManager { try { // Get the system prompt const prompt = await this.getSystemPrompt(); - + // Format all messages in conversation history const formattedMessages: any[] = []; - + // Add system prompt as first message if using formatters that expect it in messages array if (prompt && this.formatter.constructor.name === 'OpenAIMessageFormatter') { formattedMessages.push({ role: 'system', content: prompt }); } - + // Format each message in history for (const msg of this.messages) { const formatted = this.formatter.format(msg, prompt); @@ -200,8 +200,10 @@ export class ContextManager { formattedMessages.push(formatted); } } - - logger.debug(`Formatted ${formattedMessages.length} messages from history of ${this.messages.length} messages`); + + logger.debug( + `Formatted ${formattedMessages.length} messages from history of ${this.messages.length} messages` + ); return formattedMessages; } catch (error) { logger.error('Failed to format all messages', { error }); diff --git a/src/core/brain/memAgent/__test__/agent.test.ts b/src/core/brain/memAgent/__test__/agent.test.ts index 49d9af003..895970832 100644 --- a/src/core/brain/memAgent/__test__/agent.test.ts +++ b/src/core/brain/memAgent/__test__/agent.test.ts @@ -191,8 +191,8 @@ describe('MemAgent', () => { expect(error).toBeInstanceOf(ZodError); const zodError = error as ZodError; expect(zodError.issues).toHaveLength(1); - expect(zodError.issues[0].path).toEqual(['llm', 'provider']); - expect(zodError.issues[0].message).toContain('not supported'); + expect(zodError.issues[0]?.path).toEqual(['llm', 'provider']); + expect(zodError.issues[0]?.message).toContain('not supported'); } }); @@ -215,7 +215,7 @@ describe('MemAgent', () => { const invalidResult = LLMConfigSchema.safeParse(invalidLLMConfig); expect(invalidResult.success).toBe(false); if (!invalidResult.success) { - expect(invalidResult.error.issues[0].message).toContain('not supported'); + expect(invalidResult.error.issues[0]?.message).toContain('not supported'); } }); diff --git a/src/core/brain/memAgent/loader.ts b/src/core/brain/memAgent/loader.ts index 1df6514f3..f29731d46 100644 --- a/src/core/brain/memAgent/loader.ts +++ b/src/core/brain/memAgent/loader.ts @@ -9,7 +9,8 @@ function expandEnvVars(config: any): any { const expanded = config.replace( /\$([A-Z_][A-Z0-9_]*)|\${([A-Z_][A-Z0-9_]*)}/gi, (_, v1, v2) => { - return env[v1 || v2] || ''; + const key = v1 || v2; + return env[key] || ''; } ); diff --git a/src/core/brain/memAgent/state-manager.ts b/src/core/brain/memAgent/state-manager.ts index 6f1430a0d..65c9d2ba2 100644 --- a/src/core/brain/memAgent/state-manager.ts +++ b/src/core/brain/memAgent/state-manager.ts @@ -26,6 +26,11 @@ export class MemAgentStateManager { ): McpServerValidationResult { logger.debug(`Adding/updating MCP server: ${serverName}`); + // Ensure mcpServers is initialized + if (!this.runtimeConfig.mcpServers) { + this.runtimeConfig.mcpServers = {}; + } + // Validate the server configuration const existingServerNames = Object.keys(this.runtimeConfig.mcpServers); const validation = validateMcpServerConfig(serverName, serverConfig, existingServerNames); @@ -47,7 +52,7 @@ export class MemAgentStateManager { }); } - const isUpdate = serverName in this.runtimeConfig.mcpServers; + const isUpdate = this.runtimeConfig.mcpServers && serverName in this.runtimeConfig.mcpServers; // Use the validated config with defaults applied from validation result this.runtimeConfig.mcpServers[serverName] = validation.config!; @@ -59,7 +64,7 @@ export class MemAgentStateManager { public removeMcpServer(serverName: string): void { logger.debug(`Removing MCP server: ${serverName}`); - if (serverName in this.runtimeConfig.mcpServers) { + if (this.runtimeConfig.mcpServers && serverName in this.runtimeConfig.mcpServers) { delete this.runtimeConfig.mcpServers[serverName]; logger.info(`MCP server '${serverName}' removed successfully`); @@ -85,6 +90,10 @@ export class MemAgentStateManager { } public getLLMConfig(sessionId?: string): Readonly { - return this.getRuntimeConfig(sessionId).llm; + const config = this.getRuntimeConfig(sessionId); + return { + ...config.llm, + maxIterations: config.llm.maxIterations ?? 10, // Provide default value + }; } } diff --git a/src/core/env.ts b/src/core/env.ts index 45d4cce88..bcd45c30d 100644 --- a/src/core/env.ts +++ b/src/core/env.ts @@ -8,33 +8,36 @@ const EnvSchema = z.object({ // API Keys OPENAI_API_KEY: z.string().optional(), ANTHROPIC_API_KEY: z.string().optional(), - + // API Configuration OPENAI_BASE_URL: z.string().optional(), - + // Logger Configuration CIPHER_LOG_LEVEL: z.string().optional(), REDACT_SECRETS: z.string().optional(), }); -export const env: z.infer = { - // API Keys - OPENAI_API_KEY: process.env.OPENAI_API_KEY, - ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, - - // API Configuration - OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, - - // Logger Configuration - CIPHER_LOG_LEVEL: process.env.CIPHER_LOG_LEVEL, - REDACT_SECRETS: process.env.REDACT_SECRETS, -}; +// Create a dynamic env object that always reads from process.env +export const env: z.infer = new Proxy({} as z.infer, { + get(target, prop: string) { + return process.env[prop]; + } +}); export const validateEnv = () => { - const result = EnvSchema.safeParse(env); + // Create actual object from process.env for validation + const envToValidate = { + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + CIPHER_LOG_LEVEL: process.env.CIPHER_LOG_LEVEL, + REDACT_SECRETS: process.env.REDACT_SECRETS, + }; + + const result = EnvSchema.safeParse(envToValidate); if (!result.success) { // Note: logger might not be available during early initialization console.error('Invalid environment variables', result.error); } return result.success; -}; \ No newline at end of file +}; diff --git a/src/core/logger/__test__/logger.test.ts b/src/core/logger/__test__/logger.test.ts index 914de12ef..f3f9a9ac9 100644 --- a/src/core/logger/__test__/logger.test.ts +++ b/src/core/logger/__test__/logger.test.ts @@ -123,7 +123,7 @@ describe.concurrent('Special Display Features', () => { testLogger.toolCall('testTool', { foo: 'bar', baz: 123 }); expect(spyConsoleLog).toHaveBeenCalled(); let lastCall = spyConsoleLog.mock.calls[spyConsoleLog.mock.calls.length - 1]; - expect(lastCall[0]).toContain('Tool Call'); + expect(lastCall?.[0]).toContain('Tool Call'); // Reset mock for next test vi.clearAllMocks(); @@ -141,7 +141,7 @@ describe.concurrent('Special Display Features', () => { testLogger.toolResult('Simple result string'); expect(spyConsoleLog).toHaveBeenCalled(); lastCall = spyConsoleLog.mock.calls[spyConsoleLog.mock.calls.length - 1]; - expect(lastCall[0]).toContain('Tool Result'); + expect(lastCall?.[0]).toContain('Tool Result'); }); }); @@ -158,7 +158,7 @@ describe.concurrent('Special Display Features', () => { testLogger.displayAIResponse('This is an AI response'); expect(spyConsoleLog).toHaveBeenCalled(); let lastCall = spyConsoleLog.mock.calls[spyConsoleLog.mock.calls.length - 1]; - expect(lastCall[0]).toContain('AI Response'); + expect(lastCall?.[0]).toContain('AI Response'); // Test object response vi.clearAllMocks(); @@ -166,7 +166,7 @@ describe.concurrent('Special Display Features', () => { testLogger.displayAIResponse({ content: 'This is an AI response' }); expect(spyConsoleLog).toHaveBeenCalled(); lastCall = spyConsoleLog.mock.calls[spyConsoleLog.mock.calls.length - 1]; - expect(lastCall[0]).toContain('AI Response'); + expect(lastCall?.[0]).toContain('AI Response'); // ===== Test Box Display ===== vi.clearAllMocks(); @@ -176,7 +176,7 @@ describe.concurrent('Special Display Features', () => { testLogger.displayBox('Custom Title', 'Custom content here', 'blue'); expect(spyConsoleLog).toHaveBeenCalled(); lastCall = spyConsoleLog.mock.calls[spyConsoleLog.mock.calls.length - 1]; - expect(lastCall[0]).toContain('Custom Title'); + expect(lastCall?.[0]).toContain('Custom Title'); // Test default color vi.clearAllMocks(); diff --git a/src/core/logger/logger.ts b/src/core/logger/logger.ts index 9ea0ad6a7..98a1e5ad8 100644 --- a/src/core/logger/logger.ts +++ b/src/core/logger/logger.ts @@ -19,7 +19,6 @@ const logLevels = { // ===== 2. Security Layer: Data Redaction ===== -const SHOULD_REDACT = env.REDACT_SECRETS !== 'false'; const SENSITIVE_KEYS = ['apiKey', 'password', 'secret', 'token', 'auth', 'key', 'credential']; const MASK_REGEX = new RegExp( `(${SENSITIVE_KEYS.join('|')})(["']?\\s*[:=]\\s*)(["'])?.*?\\3`, @@ -27,7 +26,8 @@ const MASK_REGEX = new RegExp( ); const redactSensitiveData = (message: string): string => { - if (!SHOULD_REDACT) return message; + const shouldRedact = env.REDACT_SECRETS !== 'false'; + if (!shouldRedact) return message; return message.replace(MASK_REGEX, (match, key, separator, quote) => { const quoteMark = quote || ''; @@ -352,11 +352,17 @@ export class Logger { // ===== Utility Methods ===== createChild(options: LoggerOptions = {}): Logger { - return new Logger({ + const childOptions: LoggerOptions = { level: options.level || this.getLevel(), silent: options.silent !== undefined ? options.silent : this.isSilent, - file: options.file, - }); + }; + + // Only include file option if it's defined + if (options.file !== undefined) { + childOptions.file = options.file; + } + + return new Logger(childOptions); } // Get logger instance for advanced usage diff --git a/src/core/mcp/__test__/manager.test.ts b/src/core/mcp/__test__/manager.test.ts index d5e27e9b9..76d9ae248 100644 --- a/src/core/mcp/__test__/manager.test.ts +++ b/src/core/mcp/__test__/manager.test.ts @@ -550,7 +550,7 @@ describe('MCPManager', () => { }; mockClient.getTools.mockResolvedValue(mockTools); - mockClient.callTool.mockImplementation((name, args) => + mockClient.callTool.mockImplementation((name: string, args: any) => Promise.resolve({ result: `executed-${name}-${args.id}` }) ); diff --git a/src/core/mcp/client.ts b/src/core/mcp/client.ts index 0789867cb..aadb8d4a3 100644 --- a/src/core/mcp/client.ts +++ b/src/core/mcp/client.ts @@ -14,7 +14,6 @@ import * as path from 'path'; import * as fs from 'fs'; import { fileURLToPath } from 'url'; - import type { IMCPClient, McpServerConfig, @@ -377,21 +376,25 @@ export class MCPClient implements IMCPClient { return promptNames; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - + // Check if this is a "capability not supported" error (common for filesystem servers) - const isCapabilityError = errorMessage.includes('not implemented') || - errorMessage.includes('not supported') || - errorMessage.includes('Method not found') || - errorMessage.includes('prompts') === false; // Some servers just don't respond to prompt requests - + const isCapabilityError = + errorMessage.includes('not implemented') || + errorMessage.includes('not supported') || + errorMessage.includes('Method not found') || + errorMessage.includes('prompts') === false; // Some servers just don't respond to prompt requests + if (isCapabilityError) { - this.logger.debug(`${LOG_PREFIXES.PROMPT} Prompts not supported by server (this is normal)`, { - serverName: this.serverName, - reason: 'Server does not implement prompt capability', - }); + this.logger.debug( + `${LOG_PREFIXES.PROMPT} Prompts not supported by server (this is normal)`, + { + serverName: this.serverName, + reason: 'Server does not implement prompt capability', + } + ); return []; // Return empty array instead of throwing } - + // Real error - log as error and throw this.logger.error(`${LOG_PREFIXES.PROMPT} Failed to list prompts`, { serverName: this.serverName, @@ -463,21 +466,25 @@ export class MCPClient implements IMCPClient { return resourceUris; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - + // Check if this is a "capability not supported" error (common for filesystem servers) - const isCapabilityError = errorMessage.includes('not implemented') || - errorMessage.includes('not supported') || - errorMessage.includes('Method not found') || - errorMessage.includes('resources') === false; // Some servers just don't respond to resource requests - + const isCapabilityError = + errorMessage.includes('not implemented') || + errorMessage.includes('not supported') || + errorMessage.includes('Method not found') || + errorMessage.includes('resources') === false; // Some servers just don't respond to resource requests + if (isCapabilityError) { - this.logger.debug(`${LOG_PREFIXES.RESOURCE} Resources not supported by server (this is normal)`, { - serverName: this.serverName, - reason: 'Server does not implement resource capability', - }); + this.logger.debug( + `${LOG_PREFIXES.RESOURCE} Resources not supported by server (this is normal)`, + { + serverName: this.serverName, + reason: 'Server does not implement resource capability', + } + ); return []; // Return empty array instead of throwing } - + // Real error - log as error and throw this.logger.error(`${LOG_PREFIXES.RESOURCE} Failed to list resources`, { serverName: this.serverName, @@ -674,7 +681,7 @@ export class MCPClient implements IMCPClient { const processEnv = Object.fromEntries( Object.entries(process.env).filter(([_, value]) => value !== undefined) ) as Record; - + return { ...processEnv, ...configEnv, diff --git a/src/core/mcp/types.ts b/src/core/mcp/types.ts index cb0f0adbb..bf8962ecf 100644 --- a/src/core/mcp/types.ts +++ b/src/core/mcp/types.ts @@ -55,7 +55,7 @@ export interface StdioServerConfig extends BaseServerConfig { /** * Arguments to pass to the command. */ - args?: string[]; + args: string[]; /** * Environment variables to set for the command. diff --git a/src/core/session/session-manager.ts b/src/core/session/session-manager.ts index 76d4e4215..911a37d46 100644 --- a/src/core/session/session-manager.ts +++ b/src/core/session/session-manager.ts @@ -227,7 +227,7 @@ export class SessionManager { public async shutdown(): Promise { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); - this.cleanupInterval = undefined; + this.cleanupInterval = undefined as any; // Type assertion to handle exact optional property types } // Clear all sessions