UNPKG

clauditate

Version:

A menubar meditation app that helps you stay mindful while Claude Code works

469 lines (465 loc) 17.4 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const child_process_1 = require("child_process"); const path = __importStar(require("path")); const fs = __importStar(require("fs")); const net = __importStar(require("net")); const os = __importStar(require("os")); const preferences_1 = require("./preferences"); // Simple logger that respects development mode const isDev = process.env.NODE_ENV === 'development'; const log = { debug: (msg) => { if (isDev) console.log(msg); }, // Only in dev info: (msg) => console.log(msg), // Always show user-facing messages error: (msg) => console.error(msg), // Always show errors silent: (msg) => { if (isDev) console.log(msg); }, // Silent in production (IPC, internal) }; class ClauditateCLI { constructor() { this.isRunning = false; // Get the path to the installed package this.appPath = path.dirname(__dirname); } getSocketPath() { const tmpDir = os.tmpdir(); return path.join(tmpDir, 'clauditate.sock'); } async sendIPCCommand(command) { return new Promise((resolve, reject) => { const socket = new net.Socket(); const socketPath = this.getSocketPath(); let response = ''; socket.connect(socketPath, () => { socket.write(command); }); socket.on('data', (data) => { response += data.toString(); if (response.includes('\n')) { socket.end(); resolve(response.trim()); } }); socket.on('error', (error) => { reject(new Error(`IPC connection failed: ${error.message}`)); }); socket.on('close', () => { if (!response) { reject(new Error('No response from app')); } }); // Timeout after 5 seconds setTimeout(() => { socket.destroy(); reject(new Error('IPC command timeout')); }, 5000); }); } async run(args) { const options = this.parseArgs(args); if (options.help || args.length === 0) { this.showHelp(); return; } if (options.hookClaude) { await this.installHooks(false); return; } if (options.hookDemo) { await this.installHooks(true); return; } if (options.unhookClaude) { await this.uninstallHooks(); return; } if (options.updateHooks) { await this.updateHooks(); return; } if (options.start) { await this.startApp(); return; } if (options.show) { await this.showApp(); return; } if (options.smartShow) { await this.smartShowApp(); return; } if (options.hide) { await this.hideApp(); return; } } parseArgs(args) { const options = {}; for (const arg of args) { switch (arg) { case '--show': case '-s': options.show = true; break; case '--smart-show': options.smartShow = true; break; case '--hide': case '-h': options.hide = true; break; case '--start': options.start = true; break; case '--hook-claude': options.hookClaude = true; break; case '--hook-demo': options.hookDemo = true; break; case '--unhook-claude': options.unhookClaude = true; break; case '--update-hooks': options.updateHooks = true; break; case '--help': options.help = true; break; } } return options; } showHelp() { console.log(` clauditate - Mindful meditation for Claude Code developers Usage: clauditate [command] Commands: --start Start the meditation app --show, -s Show the meditation app (always) --smart-show Show the meditation app (only if timing is right) --hide, -h Hide the meditation app (for hooks) --hook-claude Install Claude Code hooks integration --hook-demo Install demo hooks (always show for demonstrations) --unhook-claude Remove Claude Code hooks integration --update-hooks Update Claude Code hooks to latest version --help Show this help Examples: clauditate --start # Start the app normally clauditate --hook-claude # Set up Claude Code integration clauditate --show # Show app (used by Claude hooks) clauditate --hide # Hide app (used by Claude hooks) After installation, the app will automatically appear when Claude Code is thinking! `); } async startApp() { log.info('Starting Clauditate...'); const electronPath = path.join(this.appPath, 'node_modules', '.bin', 'electron'); const mainPath = path.join(this.appPath, 'dist', 'main.js'); const child = (0, child_process_1.spawn)(electronPath, [mainPath], { stdio: 'inherit', detached: true }); child.unref(); log.info('Clauditate started successfully!'); } async showApp() { try { // Try to show via IPC first const response = await this.sendIPCCommand('show'); if (response.startsWith('error')) { log.error('Show command failed: ' + response); } else { log.silent('App shown successfully'); } } catch (error) { // If IPC fails, app is not running - respect user's choice to quit log.silent('App not running - respecting user choice to work without interruptions'); // Don't auto-start the app } } async smartShowApp() { try { // Check if app is running first const isRunning = await this.isAppRunning(); if (!isRunning) { log.silent('App not running - respecting user choice to work without interruptions'); return; } // App is running, check if we should show based on timing logic const preferencesManager = new preferences_1.PreferencesManager(); const shouldShow = await preferencesManager.shouldShowMeditation(); if (shouldShow) { const response = await this.sendIPCCommand('show'); if (response.startsWith('error')) { log.silent('Smart show command failed: ' + response); } else { log.silent('App shown based on smart timing'); } } else { log.silent('Skipping show - not the right time for mindfulness'); } } catch (error) { log.silent('Smart show failed: ' + (error instanceof Error ? error.message : String(error))); } } async demoShowApp() { try { // Check if app is running first const isRunning = await this.isAppRunning(); if (!isRunning) { log.silent('App not running - respecting user choice to work without interruptions'); return; } // Always show for demo purposes const response = await this.sendIPCCommand('show'); if (response.startsWith('error')) { log.silent('Demo show command failed: ' + response); } else { log.silent('App shown for demo'); } } catch (error) { log.silent('Demo show failed: ' + (error instanceof Error ? error.message : String(error))); } } async hideApp() { try { const response = await this.sendIPCCommand('hide'); if (response.startsWith('error')) { log.error('Hide command failed: ' + response); } else { log.silent('App hidden successfully'); } } catch (error) { log.silent('Failed to hide app: ' + String(error)); } } async isAppRunning() { try { const response = await this.sendIPCCommand('ping'); return response === 'pong'; } catch (error) { return false; } } async installHooks(demoMode = false) { log.info(demoMode ? 'Installing Claude Code demo hooks...' : 'Installing Claude Code hooks...'); const homeDir = process.env.HOME || process.env.USERPROFILE; if (!homeDir) { log.error('Could not determine home directory'); process.exit(1); } const claudeDir = path.join(homeDir, '.claude'); const settingsPath = path.join(claudeDir, 'settings.json'); // Create .claude directory if it doesn't exist if (!fs.existsSync(claudeDir)) { fs.mkdirSync(claudeDir, { recursive: true }); } // Load existing settings or create new ones let settings = {}; if (fs.existsSync(settingsPath)) { const settingsContent = fs.readFileSync(settingsPath, 'utf8'); try { settings = JSON.parse(settingsContent); } catch (error) { log.debug('Creating new settings file...'); } } // Add clauditate hooks if (!settings.hooks) { settings.hooks = {}; } const cliPath = path.join(this.appPath, 'dist', 'cli.js'); const hookCommand = demoMode ? '--show' : '--smart-show'; // Initialize PreToolUse if it doesn't exist if (!settings.hooks.PreToolUse) { settings.hooks.PreToolUse = []; } // Check if clauditate hook already exists const clauditateHookExists = settings.hooks.PreToolUse.some((hookGroup) => hookGroup.hooks.some((hook) => hook.command && hook.command.includes('clauditate'))); // Only add if it doesn't already exist if (!clauditateHookExists) { settings.hooks.PreToolUse.push({ matcher: "", hooks: [ { type: "command", command: `node "${cliPath}" ${hookCommand}` } ] }); } else { // Update existing clauditate hook settings.hooks.PreToolUse = settings.hooks.PreToolUse.map((hookGroup) => { if (hookGroup.hooks.some((hook) => hook.command && hook.command.includes('clauditate'))) { return { matcher: "", hooks: [ { type: "command", command: `node "${cliPath}" ${hookCommand}` } ] }; } return hookGroup; }); } // Remove PostToolUse hook - we don't want to hide between tools // settings.hooks.PostToolUse = [ // { // matcher: "", // hooks: [ // { // type: "command", // command: `node "${cliPath}" --hide` // } // ] // } // ]; // Remove Stop hook - let user control when to hide // settings.hooks.Stop = [ // { // matcher: "", // hooks: [ // { // type: "command", // command: `node "${cliPath}" --hide` // } // ] // } // ]; // Write settings back fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); log.info('✅ Claude Code hooks installed successfully!'); log.info('Clauditate will now appear automatically when Claude Code is thinking.'); } async updateHooks() { log.info('Updating Claude Code hooks...'); // First uninstall existing hooks await this.uninstallHooks(true); // true = silent mode // Then install fresh hooks await this.installHooks(); log.info('✅ Claude Code hooks updated successfully!'); } async uninstallHooks(silent = false) { if (!silent) { log.info('Uninstalling Claude Code hooks...'); } const homeDir = process.env.HOME || process.env.USERPROFILE; if (!homeDir) { log.error('Could not determine home directory'); process.exit(1); } const settingsPath = path.join(homeDir, '.claude', 'settings.json'); if (!fs.existsSync(settingsPath)) { if (!silent) { log.debug('No Claude Code settings found.'); } return; } const settingsContent = fs.readFileSync(settingsPath, 'utf8'); let settings = {}; try { settings = JSON.parse(settingsContent); } catch (error) { log.error('Could not parse Claude Code settings'); return; } // Remove only clauditate hooks, leave other tools' hooks intact if (settings.hooks) { // Filter out clauditate hooks from PreToolUse if (settings.hooks.PreToolUse) { settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((hookGroup) => { return !hookGroup.hooks.some((hook) => hook.command && hook.command.includes('clauditate')); }); // Remove empty PreToolUse array if (settings.hooks.PreToolUse.length === 0) { delete settings.hooks.PreToolUse; } } // Filter out clauditate hooks from PostToolUse if (settings.hooks.PostToolUse) { settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((hookGroup) => { return !hookGroup.hooks.some((hook) => hook.command && hook.command.includes('clauditate')); }); // Remove empty PostToolUse array if (settings.hooks.PostToolUse.length === 0) { delete settings.hooks.PostToolUse; } } // Filter out clauditate hooks from Stop (legacy cleanup) if (settings.hooks.Stop) { settings.hooks.Stop = settings.hooks.Stop.filter((hookGroup) => { return !hookGroup.hooks.some((hook) => hook.command && hook.command.includes('clauditate')); }); // Remove empty Stop array if (settings.hooks.Stop.length === 0) { delete settings.hooks.Stop; } } // If hooks object is empty, remove it if (Object.keys(settings.hooks).length === 0) { delete settings.hooks; } } fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); if (!silent) { log.info('✅ Claude Code hooks uninstalled successfully!'); } } } // Main execution const cli = new ClauditateCLI(); cli.run(process.argv.slice(2)).catch(console.error); //# sourceMappingURL=cli.js.map