2b60dd2932
Persist platform_source across session creation, transcript ingestion, API query paths, and viewer state so Claude and Codex data can coexist without bleeding into each other. - add platform-source normalization helpers and persist platform_source in sdk_sessions via migration 24 with backfill and indexing - thread platformSource through CLI hooks, transcript processing, context generation, pagination, search routes, SSE payloads, and session management - expose source-aware project catalogs, viewer tabs, context preview selectors, and source badges for observations, prompts, and summaries - start the transcript watcher from the worker for transcript-based clients and preserve platform source during Codex ingestion - auto-start the worker from the MCP server for MCP-only clients and tighten stdio-driven cleanup during shutdown - keep createSDKSession backward compatible with existing custom-title callers while allowing explicit platform source forwarding
247 lines
10 KiB
TypeScript
247 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';
|
|
// 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;
|
|
// 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;
|
|
CLAUDE_MEM_TRANSCRIPTS_ENABLED: string; // 'true' | 'false' - enable transcript watcher ingestion for Codex and other transcript-based clients
|
|
CLAUDE_MEM_TRANSCRIPTS_CONFIG_PATH: string; // Path to transcript watcher config JSON
|
|
// 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',
|
|
// 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',
|
|
CLAUDE_MEM_TRANSCRIPTS_ENABLED: 'true',
|
|
CLAUDE_MEM_TRANSCRIPTS_CONFIG_PATH: join(homedir(), '.claude-mem', 'transcript-watch.json'),
|
|
// 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 setting value with environment variable override.
|
|
* Priority: process.env > hardcoded default
|
|
*
|
|
* For full priority (env > settings file > default), use loadFromFile().
|
|
* This method is safe to call at module-load time (no file I/O) and still
|
|
* respects environment variable overrides that were previously ignored.
|
|
*/
|
|
static get(key: keyof SettingsDefaults): string {
|
|
return process.env[key] ?? 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());
|
|
}
|
|
}
|
|
}
|