e0fec4bad7
* feat: add terminal output control for SessionStart context Add CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT setting to control whether context is displayed in the terminal at SessionStart. When set to "false", the terminal remains clean at startup while context is still injected into Claude's system prompt. This allows users who find the context output verbose to disable it without losing the automatic context injection. Defaults to "true" for backward compatibility. Changes: - Add CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT to SettingsDefaultsManager - Check setting in context handler before setting systemMessage - Update settings file format to include new option Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: use USER_SETTINGS_PATH and skip color fetch when disabled Address PR feedback from automated review: 1. Use shared USER_SETTINGS_PATH constant instead of hardcoded path - Respects custom CLAUDE_MEM_DATA_DIR override - Consistent with other handlers (session-init, observation) 2. Skip color fetch when terminal output disabled - Check setting before making HTTP requests - Saves network round-trip on every session start Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Alan Dong <adong@Alans-MacBook-Pro.local> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
245 lines
10 KiB
TypeScript
245 lines
10 KiB
TypeScript
/**
|
|
* SettingsDefaultsManager
|
|
*
|
|
* Single source of truth for all default configuration values.
|
|
* Provides methods to get defaults with optional environment variable overrides.
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { homedir } from 'os';
|
|
import { DEFAULT_OBSERVATION_TYPES_STRING, DEFAULT_OBSERVATION_CONCEPTS_STRING } from '../constants/observation-metadata.js';
|
|
// NOTE: Do NOT import logger here - it creates a circular dependency
|
|
// logger.ts depends on SettingsDefaultsManager for its initialization
|
|
|
|
export interface SettingsDefaults {
|
|
CLAUDE_MEM_MODEL: string;
|
|
CLAUDE_MEM_CONTEXT_OBSERVATIONS: string;
|
|
CLAUDE_MEM_WORKER_PORT: string;
|
|
CLAUDE_MEM_WORKER_HOST: string;
|
|
CLAUDE_MEM_SKIP_TOOLS: string;
|
|
// AI Provider Configuration
|
|
CLAUDE_MEM_PROVIDER: string; // 'claude' | 'gemini' | 'openrouter'
|
|
CLAUDE_MEM_CLAUDE_AUTH_METHOD: string; // 'cli' | 'api' - how Claude provider authenticates
|
|
CLAUDE_MEM_GEMINI_API_KEY: string;
|
|
CLAUDE_MEM_GEMINI_MODEL: string; // 'gemini-2.5-flash-lite' | 'gemini-2.5-flash' | 'gemini-3-flash-preview'
|
|
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: string; // 'true' | 'false' - enable rate limiting for free tier
|
|
CLAUDE_MEM_OPENROUTER_API_KEY: string;
|
|
CLAUDE_MEM_OPENROUTER_MODEL: string;
|
|
CLAUDE_MEM_OPENROUTER_SITE_URL: string;
|
|
CLAUDE_MEM_OPENROUTER_APP_NAME: string;
|
|
CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES: string;
|
|
CLAUDE_MEM_OPENROUTER_MAX_TOKENS: string;
|
|
// System Configuration
|
|
CLAUDE_MEM_DATA_DIR: string;
|
|
CLAUDE_MEM_LOG_LEVEL: string;
|
|
CLAUDE_MEM_PYTHON_VERSION: string;
|
|
CLAUDE_CODE_PATH: string;
|
|
CLAUDE_MEM_MODE: string;
|
|
// Token Economics
|
|
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: string;
|
|
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: string;
|
|
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: string;
|
|
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: string;
|
|
// Observation Filtering
|
|
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: string;
|
|
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: string;
|
|
// Display Configuration
|
|
CLAUDE_MEM_CONTEXT_FULL_COUNT: string;
|
|
CLAUDE_MEM_CONTEXT_FULL_FIELD: string;
|
|
CLAUDE_MEM_CONTEXT_SESSION_COUNT: string;
|
|
// Feature Toggles
|
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: string;
|
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: string;
|
|
CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT: string;
|
|
CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: string;
|
|
// Process Management
|
|
CLAUDE_MEM_MAX_CONCURRENT_AGENTS: string; // Max concurrent Claude SDK agent subprocesses (default: 2)
|
|
// Exclusion Settings
|
|
CLAUDE_MEM_EXCLUDED_PROJECTS: string; // Comma-separated glob patterns for excluded project paths
|
|
CLAUDE_MEM_FOLDER_MD_EXCLUDE: string; // JSON array of folder paths to exclude from CLAUDE.md generation
|
|
// Chroma Vector Database Configuration
|
|
CLAUDE_MEM_CHROMA_ENABLED: string; // 'true' | 'false' - set to 'false' for SQLite-only mode
|
|
CLAUDE_MEM_CHROMA_MODE: string; // 'local' | 'remote'
|
|
CLAUDE_MEM_CHROMA_HOST: string;
|
|
CLAUDE_MEM_CHROMA_PORT: string;
|
|
CLAUDE_MEM_CHROMA_SSL: string;
|
|
// Future cloud support
|
|
CLAUDE_MEM_CHROMA_API_KEY: string;
|
|
CLAUDE_MEM_CHROMA_TENANT: string;
|
|
CLAUDE_MEM_CHROMA_DATABASE: string;
|
|
}
|
|
|
|
export class SettingsDefaultsManager {
|
|
/**
|
|
* Default values for all settings
|
|
*/
|
|
private static readonly DEFAULTS: SettingsDefaults = {
|
|
CLAUDE_MEM_MODEL: 'claude-sonnet-4-5',
|
|
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
|
|
CLAUDE_MEM_WORKER_PORT: '37777',
|
|
CLAUDE_MEM_WORKER_HOST: '127.0.0.1',
|
|
CLAUDE_MEM_SKIP_TOOLS: 'ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion',
|
|
// AI Provider Configuration
|
|
CLAUDE_MEM_PROVIDER: 'claude', // Default to Claude
|
|
CLAUDE_MEM_CLAUDE_AUTH_METHOD: 'cli', // Default to CLI subscription billing (not API key)
|
|
CLAUDE_MEM_GEMINI_API_KEY: '', // Empty by default, can be set via UI or env
|
|
CLAUDE_MEM_GEMINI_MODEL: 'gemini-2.5-flash-lite', // Default Gemini model (highest free tier RPM)
|
|
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: 'true', // Rate limiting ON by default for free tier users
|
|
CLAUDE_MEM_OPENROUTER_API_KEY: '', // Empty by default, can be set via UI or env
|
|
CLAUDE_MEM_OPENROUTER_MODEL: 'xiaomi/mimo-v2-flash:free', // Default OpenRouter model (free tier)
|
|
CLAUDE_MEM_OPENROUTER_SITE_URL: '', // Optional: for OpenRouter analytics
|
|
CLAUDE_MEM_OPENROUTER_APP_NAME: 'claude-mem', // App name for OpenRouter analytics
|
|
CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES: '20', // Max messages in context window
|
|
CLAUDE_MEM_OPENROUTER_MAX_TOKENS: '100000', // Max estimated tokens (~100k safety limit)
|
|
// System Configuration
|
|
CLAUDE_MEM_DATA_DIR: join(homedir(), '.claude-mem'),
|
|
CLAUDE_MEM_LOG_LEVEL: 'INFO',
|
|
CLAUDE_MEM_PYTHON_VERSION: '3.13',
|
|
CLAUDE_CODE_PATH: '', // Empty means auto-detect via 'which claude'
|
|
CLAUDE_MEM_MODE: 'code', // Default mode profile
|
|
// Token Economics
|
|
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'false',
|
|
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'false',
|
|
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'false',
|
|
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
|
|
// Observation Filtering
|
|
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: DEFAULT_OBSERVATION_TYPES_STRING,
|
|
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: DEFAULT_OBSERVATION_CONCEPTS_STRING,
|
|
// Display Configuration
|
|
CLAUDE_MEM_CONTEXT_FULL_COUNT: '0',
|
|
CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
|
|
CLAUDE_MEM_CONTEXT_SESSION_COUNT: '10',
|
|
// Feature Toggles
|
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
|
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
|
|
CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT: 'true',
|
|
CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: 'false',
|
|
// Process Management
|
|
CLAUDE_MEM_MAX_CONCURRENT_AGENTS: '2', // Max concurrent Claude SDK agent subprocesses
|
|
// Exclusion Settings
|
|
CLAUDE_MEM_EXCLUDED_PROJECTS: '', // Comma-separated glob patterns for excluded project paths
|
|
CLAUDE_MEM_FOLDER_MD_EXCLUDE: '[]', // JSON array of folder paths to exclude from CLAUDE.md generation
|
|
// Chroma Vector Database Configuration
|
|
CLAUDE_MEM_CHROMA_ENABLED: 'true', // Set to 'false' to disable Chroma and use SQLite-only search
|
|
CLAUDE_MEM_CHROMA_MODE: 'local', // 'local' uses persistent chroma-mcp via uvx, 'remote' connects to existing server
|
|
CLAUDE_MEM_CHROMA_HOST: '127.0.0.1',
|
|
CLAUDE_MEM_CHROMA_PORT: '8000',
|
|
CLAUDE_MEM_CHROMA_SSL: 'false',
|
|
// Future cloud support (claude-mem pro)
|
|
CLAUDE_MEM_CHROMA_API_KEY: '',
|
|
CLAUDE_MEM_CHROMA_TENANT: 'default_tenant',
|
|
CLAUDE_MEM_CHROMA_DATABASE: 'default_database',
|
|
};
|
|
|
|
/**
|
|
* Get all defaults as an object
|
|
*/
|
|
static getAllDefaults(): SettingsDefaults {
|
|
return { ...this.DEFAULTS };
|
|
}
|
|
|
|
/**
|
|
* Get a default value from defaults (no environment variable override)
|
|
*/
|
|
static get(key: keyof SettingsDefaults): string {
|
|
return this.DEFAULTS[key];
|
|
}
|
|
|
|
/**
|
|
* Get an integer default value
|
|
*/
|
|
static getInt(key: keyof SettingsDefaults): number {
|
|
const value = this.get(key);
|
|
return parseInt(value, 10);
|
|
}
|
|
|
|
/**
|
|
* Get a boolean default value
|
|
* Handles both string 'true' and boolean true from JSON
|
|
*/
|
|
static getBool(key: keyof SettingsDefaults): boolean {
|
|
const value = this.get(key);
|
|
return value === 'true' || value === true;
|
|
}
|
|
|
|
/**
|
|
* Apply environment variable overrides to settings
|
|
* Environment variables take highest priority over file and defaults
|
|
*/
|
|
private static applyEnvOverrides(settings: SettingsDefaults): SettingsDefaults {
|
|
const result = { ...settings };
|
|
for (const key of Object.keys(this.DEFAULTS) as Array<keyof SettingsDefaults>) {
|
|
if (process.env[key] !== undefined) {
|
|
result[key] = process.env[key]!;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Load settings from file with fallback to defaults
|
|
* Returns merged settings with proper priority: process.env > settings file > defaults
|
|
* Handles all errors (missing file, corrupted JSON, permissions) gracefully
|
|
*
|
|
* Configuration Priority:
|
|
* 1. Environment variables (highest priority)
|
|
* 2. Settings file (~/.claude-mem/settings.json)
|
|
* 3. Default values (lowest priority)
|
|
*/
|
|
static loadFromFile(settingsPath: string): SettingsDefaults {
|
|
try {
|
|
if (!existsSync(settingsPath)) {
|
|
const defaults = this.getAllDefaults();
|
|
try {
|
|
const dir = dirname(settingsPath);
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
writeFileSync(settingsPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
// Use console instead of logger to avoid circular dependency
|
|
console.log('[SETTINGS] Created settings file with defaults:', settingsPath);
|
|
} catch (error) {
|
|
console.warn('[SETTINGS] Failed to create settings file, using in-memory defaults:', settingsPath, error);
|
|
}
|
|
// Still apply env var overrides even when file doesn't exist
|
|
return this.applyEnvOverrides(defaults);
|
|
}
|
|
|
|
const settingsData = readFileSync(settingsPath, 'utf-8');
|
|
const settings = JSON.parse(settingsData);
|
|
|
|
// MIGRATION: Handle old nested schema { env: {...} }
|
|
let flatSettings = settings;
|
|
if (settings.env && typeof settings.env === 'object') {
|
|
// Migrate from nested to flat schema
|
|
flatSettings = settings.env;
|
|
|
|
// Auto-migrate the file to flat schema
|
|
try {
|
|
writeFileSync(settingsPath, JSON.stringify(flatSettings, null, 2), 'utf-8');
|
|
console.log('[SETTINGS] Migrated settings file from nested to flat schema:', settingsPath);
|
|
} catch (error) {
|
|
console.warn('[SETTINGS] Failed to auto-migrate settings file:', settingsPath, error);
|
|
// Continue with in-memory migration even if write fails
|
|
}
|
|
}
|
|
|
|
// Merge file settings with defaults (flat schema)
|
|
const result: SettingsDefaults = { ...this.DEFAULTS };
|
|
for (const key of Object.keys(this.DEFAULTS) as Array<keyof SettingsDefaults>) {
|
|
if (flatSettings[key] !== undefined) {
|
|
result[key] = flatSettings[key];
|
|
}
|
|
}
|
|
|
|
// Apply environment variable overrides (highest priority)
|
|
return this.applyEnvOverrides(result);
|
|
} catch (error) {
|
|
console.warn('[SETTINGS] Failed to load settings, using defaults:', settingsPath, error);
|
|
// Still apply env var overrides even on error
|
|
return this.applyEnvOverrides(this.getAllDefaults());
|
|
}
|
|
}
|
|
}
|