UNPKG

clauded

Version:

Tame Claude - reduce risk and time wasting. Confidence validation is just the first step in comprehensive AI behavior management.

455 lines (388 loc) • 16.5 kB
import fs from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import { homedir } from 'os'; import chalk from 'chalk'; import { getShellConfig } from './detector.js'; const CLAUDE_DIR = path.join(homedir(), '.claude'); const CLAUDED_DIR = path.join(CLAUDE_DIR, 'clauded'); const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json'); async function createBackup() { const backup = { settingsExists: false, settingsContent: null, claudedDirExists: false, timestamp: new Date().toISOString() }; try { // Check if settings file exists and backup its content if (existsSync(SETTINGS_FILE)) { backup.settingsExists = true; backup.settingsContent = await fs.readFile(SETTINGS_FILE, 'utf8'); } // Check if clauded directory exists if (existsSync(CLAUDED_DIR)) { backup.claudedDirExists = true; } console.log(chalk.gray('šŸ“‹ Created installation backup')); return backup; } catch (error) { console.log(chalk.yellow('āš ļø Could not create backup, proceeding without rollback capability')); return backup; } } async function rollbackChanges(backup) { try { console.log(chalk.yellow('šŸ”„ Rolling back installation changes...')); // Remove clauded directory if it didn't exist before if (!backup.claudedDirExists && existsSync(CLAUDED_DIR)) { await fs.rm(CLAUDED_DIR, { recursive: true, force: true }); console.log(chalk.gray(' • Removed clauded directory')); } // Restore settings file if (backup.settingsExists && backup.settingsContent) { await fs.writeFile(SETTINGS_FILE, backup.settingsContent); console.log(chalk.gray(' • Restored settings.json')); } else if (!backup.settingsExists && existsSync(SETTINGS_FILE)) { // Remove settings file if it didn't exist before await fs.unlink(SETTINGS_FILE); console.log(chalk.gray(' • Removed settings.json')); } // Remove from PATH (best effort) try { const { getShellConfig } = await import('./detector.js'); const shellConfig = getShellConfig(); let content = await fs.readFile(shellConfig, 'utf8'); const lines = content.split('\n'); const filtered = lines.filter(line => !line.includes('clauded') && !line.includes('clauded/scripts') ); if (filtered.length !== lines.length) { await fs.writeFile(shellConfig, filtered.join('\n')); console.log(chalk.gray(' • Removed from PATH')); } } catch (error) { // PATH cleanup is best effort, don't fail rollback } console.log(chalk.green('āœ… Rollback completed successfully')); } catch (error) { console.log(chalk.red('āŒ Rollback failed:'), error.message); console.log(chalk.yellow(' Manual cleanup may be required')); } } export async function installClaudedSystem(config) { const backupData = await createBackup(); try { // Ensure directories exist await ensureDirectories(); // Install unified hooks (consolidates 4 hooks into 2 for better performance) await installUnifiedPromptHook(config); await installUnifiedPostToolHook(); // Install clauded command script await installClaudedCommand(); // Update Claude settings to include all hooks await updateClaudeSettings(); // Add clauded command to PATH await addToPath(); console.log(chalk.green('āœ… Installation completed successfully')); } catch (error) { console.log(chalk.red('āŒ Installation failed, rolling back changes...')); await rollbackChanges(backupData); throw error; } } async function ensureDirectories() { await fs.mkdir(path.join(CLAUDED_DIR, 'hooks'), { recursive: true }); await fs.mkdir(path.join(CLAUDED_DIR, 'scripts'), { recursive: true }); } async function installUnifiedPromptHook(_config) { const hookPath = path.join(CLAUDED_DIR, 'hooks', 'confidence-unified-prompt.py'); const sourcePath = path.join(path.dirname(new URL(import.meta.url).pathname), 'confidence-unified-prompt.py'); const configCachePath = path.join(CLAUDED_DIR, 'hooks', 'config-cache.py'); const configCacheSource = path.join(path.dirname(new URL(import.meta.url).pathname), 'config-cache.py'); try { await fs.copyFile(sourcePath, hookPath); await fs.chmod(hookPath, 0o755); // Make executable await fs.copyFile(configCacheSource, configCachePath); await fs.chmod(configCachePath, 0o755); // Make executable console.log(chalk.green('āœ“ Installed unified prompt hook (UserPromptSubmit)')); } catch (error) { throw new Error(`Failed to install unified prompt hook: ${error.message}`); } } async function installUnifiedPostToolHook() { const hookPath = path.join(CLAUDED_DIR, 'hooks', 'confidence-unified-posttool.py'); const sourcePath = path.join(path.dirname(new URL(import.meta.url).pathname), 'confidence-unified-posttool.py'); try { await fs.copyFile(sourcePath, hookPath); await fs.chmod(hookPath, 0o755); // Make executable console.log(chalk.green('āœ“ Installed unified PostToolUse hook')); } catch (error) { throw new Error(`Failed to install unified PostToolUse hook: ${error.message}`); } } async function installConfidenceNotificationHook() { const hookPath = path.join(CLAUDED_DIR, 'hooks', 'confidence-notification-hook.py'); const sourcePath = path.join(path.dirname(new URL(import.meta.url).pathname), 'confidence-notification-hook.py'); try { // Copy the notification hook file await fs.copyFile(sourcePath, hookPath); await fs.chmod(hookPath, 0o755); console.log(chalk.green('āœ“ Installed confidence notification hook')); } catch (error) { console.log(chalk.red(`āŒ Failed to install notification hook: ${error.message}`)); throw error; } } async function installClaudedCommand() { const commandPath = path.join(CLAUDED_DIR, 'scripts', 'clauded'); const sourcePath = path.join(process.cwd(), 'bin', 'clauded.js'); try { // Copy the clauded.js file to the scripts directory await fs.copyFile(sourcePath, commandPath); console.log(chalk.green('āœ“ Installed clauded command')); } catch (error) { console.log(chalk.yellow('āš ļø Could not install clauded command (using npm link instead)')); } } async function updateClaudeSettings() { const settingsPath = path.join(homedir(), '.claude', 'settings.json'); try { let settings = {}; if (existsSync(settingsPath)) { const content = await fs.readFile(settingsPath, 'utf8'); settings = JSON.parse(content); } // Initialize hooks if they don't exist if (!settings.hooks) { settings.hooks = {}; } // Initialize UserPromptSubmit if it doesn't exist if (!settings.hooks.UserPromptSubmit) { settings.hooks.UserPromptSubmit = []; } // Initialize PostToolUse if it doesn't exist if (!settings.hooks.PostToolUse) { settings.hooks.PostToolUse = []; } // Initialize Notification if it doesn't exist if (!settings.hooks.Notification) { settings.hooks.Notification = []; } // Define hook paths const validatorPath = path.join(homedir(), '.claude', 'clauded', 'hooks', 'confidence-validator.py'); const scorerPath = path.join(homedir(), '.claude', 'clauded', 'hooks', 'confidence-scorer.py'); const displayPath = path.join(homedir(), '.claude', 'clauded', 'hooks', 'confidence-score-display.py'); const unifiedPath = path.join(homedir(), '.claude', 'clauded', 'hooks', 'confidence-unified-prompt.py'); // Remove old hooks from UserPromptSubmit const oldHookPaths = [validatorPath, displayPath]; settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(hookGroup => { if (!hookGroup.hooks) return true; hookGroup.hooks = hookGroup.hooks.filter(hook => !oldHookPaths.includes(hook.command)); return hookGroup.hooks.length > 0; }); // Remove old scorer hook from PostToolUse settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hookGroup => { if (!hookGroup.hooks) return true; hookGroup.hooks = hookGroup.hooks.filter(hook => hook.command !== scorerPath); return hookGroup.hooks.length > 0; }); // Check if unified hook is already registered const existingUnifiedHook = settings.hooks.UserPromptSubmit.find(hookGroup => hookGroup.hooks && hookGroup.hooks.some(hook => hook.command === unifiedPath ) ); // Check if notification hook is already registered const notificationPath = path.join(homedir(), '.claude', 'clauded', 'hooks', 'confidence-notification-hook.py'); const existingNotificationHook = settings.hooks.Notification.find(hookGroup => hookGroup.hooks && hookGroup.hooks.some(hook => hook.command === notificationPath ) ); let modified = true; // Always true since we cleaned up old hooks if (!existingUnifiedHook) { // Add unified confidence hook to UserPromptSubmit settings.hooks.UserPromptSubmit.push({ hooks: [ { type: 'command', command: unifiedPath } ] }); console.log('āœ… Unified confidence hook added to UserPromptSubmit'); } else { console.log('āœ… Unified confidence hook already registered'); } if (!existingNotificationHook) { // Add confidence notification hook to Notification settings.hooks.Notification.push({ hooks: [ { type: 'command', command: notificationPath } ] }); modified = true; console.log('āœ… Confidence notification hook added to Notification'); } else { console.log('āœ… Confidence notification hook already registered'); } if (modified) { await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); } } catch (error) { console.error('āŒ Error updating Claude settings:', error.message); throw error; } } async function addToPath() { const shellConfig = getShellConfig(); const scriptsPath = path.join(CLAUDED_DIR, 'scripts'); try { let content = await fs.readFile(shellConfig, 'utf8'); // Check if already added if (content.includes('clauded')) { return; } // Add to PATH const pathLine = `\n# Clauded - Added by clauded\nexport PATH="${scriptsPath}:$PATH"\n`; content += pathLine; await fs.writeFile(shellConfig, content); console.log(chalk.yellow(`\nšŸ“ Added clauded command to ${path.basename(shellConfig)}`)); console.log(chalk.cyan('\nšŸ’” To use immediately, run:')); console.log(chalk.white.bold(` source ${shellConfig}\n`)); } catch (error) { console.error(chalk.red(`\nāš ļø Could not add to PATH: ${error.message}`)); console.log(chalk.gray(' You can manually add this to your shell config:')); console.log(chalk.cyan(` export PATH="${scriptsPath}:$PATH"`)); } } export async function uninstall() { console.log(chalk.yellow('\nšŸ—‘ļø Uninstalling Clauded...\n')); try { // Remove hooks from settings try { const content = await fs.readFile(SETTINGS_FILE, 'utf8'); const settings = JSON.parse(content); if (settings.hooks) { let modified = false; // Handle PostToolUse hooks (remove both validator and scorer) if (settings.hooks.PostToolUse) { if (Array.isArray(settings.hooks.PostToolUse)) { settings.hooks.PostToolUse.forEach(postToolHook => { if (postToolHook.hooks && Array.isArray(postToolHook.hooks)) { const filteredHooks = postToolHook.hooks.filter(hook => !hook.command || (!hook.command.includes('confidence-validator.py') && !hook.command.includes('confidence-scorer.py')) ); if (filteredHooks.length !== postToolHook.hooks.length) { postToolHook.hooks = filteredHooks; modified = true; } } }); // Remove empty PostToolUse hooks settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(postToolHook => postToolHook.hooks && postToolHook.hooks.length > 0 ); if (settings.hooks.PostToolUse.length === 0) { delete settings.hooks.PostToolUse; } } } // Handle Stop hooks (for backward compatibility) if (settings.hooks.Stop) { // Handle array format if (Array.isArray(settings.hooks.Stop)) { settings.hooks.Stop.forEach(stopHook => { if (stopHook.hooks && Array.isArray(stopHook.hooks)) { const filteredHooks = stopHook.hooks.filter(hook => !hook.command || !hook.command.includes('confidence-validator.py') ); if (filteredHooks.length !== stopHook.hooks.length) { stopHook.hooks = filteredHooks; modified = true; } } }); // Remove empty Stop hooks settings.hooks.Stop = settings.hooks.Stop.filter(stopHook => stopHook.hooks && stopHook.hooks.length > 0 ); if (settings.hooks.Stop.length === 0) { delete settings.hooks.Stop; } } else if (typeof settings.hooks.Stop === 'string' && settings.hooks.Stop.includes('confidence-validator.py')) { // Handle old string format delete settings.hooks.Stop; modified = true; } } // Handle Before hooks (for backward compatibility) if (settings.hooks.Before) { // Handle array format if (Array.isArray(settings.hooks.Before)) { settings.hooks.Before.forEach(beforeHook => { if (beforeHook.hooks && Array.isArray(beforeHook.hooks)) { const filteredHooks = beforeHook.hooks.filter(hook => !hook.command || !hook.command.includes('confidence-validator.py') ); if (filteredHooks.length !== beforeHook.hooks.length) { beforeHook.hooks = filteredHooks; modified = true; } } }); // Remove empty Before hooks settings.hooks.Before = settings.hooks.Before.filter(beforeHook => beforeHook.hooks && beforeHook.hooks.length > 0 ); if (settings.hooks.Before.length === 0) { delete settings.hooks.Before; } } else if (typeof settings.hooks.Before === 'string' && settings.hooks.Before.includes('confidence-validator.py')) { // Handle old string format delete settings.hooks.Before; modified = true; } } if (modified) { await fs.writeFile(SETTINGS_FILE, JSON.stringify(settings, null, 2)); console.log(chalk.green('āœ“ Removed confidence validator hook from settings')); } } } catch (error) { // Settings file doesn't exist or is invalid } // Remove from PATH const shellConfig = getShellConfig(); try { let content = await fs.readFile(shellConfig, 'utf8'); const lines = content.split('\n'); const filtered = lines.filter(line => !line.includes('clauded') && !line.includes('clauded/scripts') ); if (filtered.length !== lines.length) { await fs.writeFile(shellConfig, filtered.join('\n')); console.log(chalk.green('āœ“ Removed from PATH')); } } catch (error) { // Shell config doesn't exist } // Remove clauded directory try { await fs.rm(CLAUDED_DIR, { recursive: true, force: true }); console.log(chalk.green('āœ“ Removed clauded directory')); } catch (error) { // Directory doesn't exist } console.log(chalk.green('\nāœ… Clauded uninstalled successfully!\n')); } catch (error) { console.error(chalk.red('āŒ Error during uninstall:'), error.message); throw error; } }