diff --git a/bin/install.js b/bin/install.js index a8d2af9b5..dcaeeca89 100755 --- a/bin/install.js +++ b/bin/install.js @@ -55,8 +55,6 @@ function parseConfigDirArg() { const explicitConfigDir = parseConfigDirArg(); const hasHelp = args.includes('--help') || args.includes('-h'); const forceStatusline = args.includes('--force-statusline'); -const forceNotify = args.includes('--force-notify'); -const noNotify = args.includes('--no-notify'); console.log(banner); @@ -70,8 +68,6 @@ if (hasHelp) { ${cyan}-c, --config-dir ${reset} Specify custom Claude config directory ${cyan}-h, --help${reset} Show this help message ${cyan}--force-statusline${reset} Replace existing statusline config - ${cyan}--force-notify${reset} Replace existing notification hook - ${cyan}--no-notify${reset} Skip notification hook installation ${yellow}Examples:${reset} ${dim}# Install to default ~/.claude directory${reset} @@ -221,10 +217,6 @@ function install(isGlobal) { const srcFile = path.join(hooksSrc, entry); const destFile = path.join(hooksDest, entry); fs.copyFileSync(srcFile, destFile); - // Make shell scripts executable - if (entry.endsWith('.sh')) { - fs.chmodSync(destFile, 0o755); - } } console.log(` ${green}✓${reset} Installed hooks`); } @@ -233,14 +225,11 @@ function install(isGlobal) { const settingsPath = path.join(claudeDir, 'settings.json'); const settings = readSettings(settingsPath); const statuslineCommand = isGlobal - ? '$HOME/.claude/hooks/statusline.sh' - : '.claude/hooks/statusline.sh'; + ? 'node "$HOME/.claude/hooks/statusline.js"' + : 'node .claude/hooks/statusline.js'; const updateCheckCommand = isGlobal - ? '$HOME/.claude/hooks/gsd-check-update.sh' - : '.claude/hooks/gsd-check-update.sh'; - const notifyCommand = isGlobal - ? '$HOME/.claude/hooks/gsd-notify.sh' - : '.claude/hooks/gsd-notify.sh'; + ? 'node "$HOME/.claude/hooks/gsd-check-update.js"' + : 'node .claude/hooks/gsd-check-update.js'; // Configure SessionStart hook for update checking if (!settings.hooks) { @@ -267,13 +256,13 @@ function install(isGlobal) { console.log(` ${green}✓${reset} Configured update check hook`); } - return { settingsPath, settings, statuslineCommand, notifyCommand }; + return { settingsPath, settings, statuslineCommand }; } /** - * Apply statusline and notification config, then print completion message + * Apply statusline config, then print completion message */ -function finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify) { +function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline) { if (shouldInstallStatusline) { settings.statusLine = { type: 'command', @@ -282,25 +271,6 @@ function finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, console.log(` ${green}✓${reset} Configured statusline`); } - if (shouldInstallNotify) { - if (!settings.hooks.Stop) { - settings.hooks.Stop = []; - } - // Remove any existing GSD notify hook first - settings.hooks.Stop = settings.hooks.Stop.filter(entry => - !(entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-notify'))) - ); - settings.hooks.Stop.push({ - hooks: [ - { - type: 'command', - command: notifyCommand - } - ] - }); - console.log(` ${green}✓${reset} Configured completion notifications`); - } - // Always write settings (hooks were already configured in install()) writeSettings(settingsPath, settings); @@ -365,66 +335,6 @@ function handleStatusline(settings, isInteractive, callback) { }); } -/** - * Handle notification hook configuration with optional prompt - */ -function handleNotifications(settings, isInteractive, callback) { - // Check if --no-notify flag was passed - if (noNotify) { - callback(false); - return; - } - - // Check if GSD notify hook already exists - const hasExisting = settings.hooks?.Stop?.some(entry => - entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-notify')) - ); - - // No existing - just install it - if (!hasExisting) { - callback(true); - return; - } - - // Has existing and --force-notify flag - if (forceNotify) { - callback(true); - return; - } - - // Has existing, non-interactive mode - skip - if (!isInteractive) { - console.log(` ${yellow}⚠${reset} Skipping notifications (already configured)`); - console.log(` Use ${cyan}--force-notify${reset} to replace\n`); - callback(false); - return; - } - - // Has existing, interactive mode - prompt user - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - console.log(` - ${yellow}⚠${reset} Existing notification hook detected - - GSD includes completion notifications that alert you when: - • A phase completes planning or execution - • Claude stops and needs your input - • Works on Mac, Linux, and Windows - - ${cyan}1${reset}) Keep existing - ${cyan}2${reset}) Replace with GSD notifications -`); - - rl.question(` Choice ${dim}[1]${reset}: `, (answer) => { - rl.close(); - const choice = answer.trim() || '1'; - callback(choice === '2'); - }); -} - /** * Prompt for install location */ @@ -448,12 +358,10 @@ function promptLocation() { rl.close(); const choice = answer.trim() || '1'; const isGlobal = choice !== '2'; - const { settingsPath, settings, statuslineCommand, notifyCommand } = install(isGlobal); + const { settingsPath, settings, statuslineCommand } = install(isGlobal); // Interactive mode - prompt for optional features handleStatusline(settings, true, (shouldInstallStatusline) => { - handleNotifications(settings, true, (shouldInstallNotify) => { - finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify); - }); + finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline); }); }); } @@ -466,20 +374,16 @@ if (hasGlobal && hasLocal) { console.error(` ${yellow}Cannot use --config-dir with --local${reset}`); process.exit(1); } else if (hasGlobal) { - const { settingsPath, settings, statuslineCommand, notifyCommand } = install(true); + const { settingsPath, settings, statuslineCommand } = install(true); // Non-interactive - respect flags handleStatusline(settings, false, (shouldInstallStatusline) => { - handleNotifications(settings, false, (shouldInstallNotify) => { - finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify); - }); + finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline); }); } else if (hasLocal) { - const { settingsPath, settings, statuslineCommand, notifyCommand } = install(false); + const { settingsPath, settings, statuslineCommand } = install(false); // Non-interactive - respect flags handleStatusline(settings, false, (shouldInstallStatusline) => { - handleNotifications(settings, false, (shouldInstallNotify) => { - finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify); - }); + finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline); }); } else { promptLocation(); diff --git a/hooks/gsd-check-update.js b/hooks/gsd-check-update.js new file mode 100755 index 000000000..df83bb929 --- /dev/null +++ b/hooks/gsd-check-update.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// Check for GSD updates in background, write result to cache +// Called by SessionStart hook - runs once per session + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { execSync, spawn } = require('child_process'); + +const homeDir = os.homedir(); +const cacheDir = path.join(homeDir, '.claude', 'cache'); +const cacheFile = path.join(cacheDir, 'gsd-update-check.json'); +const versionFile = path.join(homeDir, '.claude', 'get-shit-done', 'VERSION'); + +// Ensure cache directory exists +if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }); +} + +// Run check in background (spawn detached process) +const child = spawn(process.execPath, ['-e', ` + const fs = require('fs'); + const { execSync } = require('child_process'); + + const cacheFile = ${JSON.stringify(cacheFile)}; + const versionFile = ${JSON.stringify(versionFile)}; + + let installed = '0.0.0'; + try { + installed = fs.readFileSync(versionFile, 'utf8').trim(); + } catch (e) {} + + let latest = null; + try { + latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000 }).trim(); + } catch (e) {} + + const result = { + update_available: latest && installed !== latest, + installed, + latest: latest || 'unknown', + checked: Math.floor(Date.now() / 1000) + }; + + fs.writeFileSync(cacheFile, JSON.stringify(result)); +`], { + detached: true, + stdio: 'ignore' +}); + +child.unref(); diff --git a/hooks/gsd-check-update.sh b/hooks/gsd-check-update.sh deleted file mode 100755 index faf5f080b..000000000 --- a/hooks/gsd-check-update.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# Check for GSD updates in background, write result to cache -# Called by SessionStart hook - runs once per session - -CACHE_FILE="$HOME/.claude/cache/gsd-update-check.json" -mkdir -p "$HOME/.claude/cache" - -# Run check in background (non-blocking) -( - installed=$(cat "$HOME/.claude/get-shit-done/VERSION" 2>/dev/null || echo "0.0.0") - latest=$(npm view get-shit-done-cc version 2>/dev/null) - - if [[ -n "$latest" && "$installed" != "$latest" ]]; then - echo "{\"update_available\":true,\"installed\":\"$installed\",\"latest\":\"$latest\",\"checked\":$(date +%s)}" > "$CACHE_FILE" - else - echo "{\"update_available\":false,\"installed\":\"$installed\",\"latest\":\"${latest:-unknown}\",\"checked\":$(date +%s)}" > "$CACHE_FILE" - fi -) & - -exit 0 diff --git a/hooks/gsd-notify.sh b/hooks/gsd-notify.sh deleted file mode 100755 index e5edce3da..000000000 --- a/hooks/gsd-notify.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -# GSD Completion Notification Hook -# Cross-platform alert when Claude stops (task complete, needs input, etc.) - -input=$(cat) -session_id=$(echo "$input" | jq -r '.session_id // empty') -cwd=$(echo "$input" | jq -r '.cwd // empty') -transcript_path=$(echo "$input" | jq -r '.transcript_path // empty') - -# Extract project name -project="Claude Code" -if [[ -n "$cwd" ]]; then - project=$(basename "$cwd") -fi - -# Check todo list for current/completed task -message="" -if [[ -n "$session_id" ]]; then - todo_file=$(ls -t "$HOME/.claude/todos/${session_id}"*.json 2>/dev/null | head -1) - if [[ -f "$todo_file" ]]; then - # Get most recently completed task, or in-progress task - completed=$(jq -r '[.[] | select(.status=="completed")] | last | .content // empty' "$todo_file" 2>/dev/null) - if [[ -n "$completed" ]]; then - message="Completed: $completed" - else - in_progress=$(jq -r '.[] | select(.status=="in_progress") | .content' "$todo_file" 2>/dev/null | head -1) - if [[ -n "$in_progress" ]]; then - message="Paused: $in_progress" - fi - fi - fi -fi - -# Fallback: generic message -if [[ -z "$message" ]]; then - message="Ready for input" -fi - -# Send notification based on OS -case "$(uname -s)" in - Darwin) - osascript -e "display alert \"GSD: $project\" message \"$message\" as informational" &>/dev/null & - ;; - Linux) - if command -v notify-send &>/dev/null; then - notify-send "GSD: $project" "$message" --urgency=normal - elif command -v zenity &>/dev/null; then - zenity --info --title="GSD: $project" --text="$message" & - fi - ;; - MINGW*|CYGWIN*|MSYS*) - # Windows via PowerShell - if command -v powershell.exe &>/dev/null; then - powershell.exe -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('$message', 'GSD: $project', 'OK', 'Information')" &>/dev/null & - fi - ;; -esac - -exit 0 diff --git a/hooks/statusline.js b/hooks/statusline.js new file mode 100755 index 000000000..400e7eaca --- /dev/null +++ b/hooks/statusline.js @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Claude Code Statusline - GSD Edition +// Shows: model | current task | directory | context usage + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Read JSON from stdin +let input = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => input += chunk); +process.stdin.on('end', () => { + try { + const data = JSON.parse(input); + const model = data.model?.display_name || 'Claude'; + const dir = data.workspace?.current_dir || process.cwd(); + const session = data.session_id || ''; + const remaining = data.context_window?.remaining_percentage; + + // Context window display (shows USED percentage) + let ctx = ''; + if (remaining != null) { + const rem = Math.round(remaining); + const used = 100 - rem; + + // Build progress bar (10 segments) + const filled = Math.floor(used / 10); + const bar = '█'.repeat(filled) + '░'.repeat(10 - filled); + + // Color based on usage + if (used < 50) { + ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`; + } else if (used < 65) { + ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`; + } else if (used < 80) { + ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`; + } else { + ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`; + } + } + + // Current task from todos + let task = ''; + const homeDir = os.homedir(); + const todosDir = path.join(homeDir, '.claude', 'todos'); + if (session && fs.existsSync(todosDir)) { + const files = fs.readdirSync(todosDir) + .filter(f => f.startsWith(session) && f.includes('-agent-') && f.endsWith('.json')) + .map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime })) + .sort((a, b) => b.mtime - a.mtime); + + if (files.length > 0) { + try { + const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8')); + const inProgress = todos.find(t => t.status === 'in_progress'); + if (inProgress) task = inProgress.activeForm || ''; + } catch (e) {} + } + } + + // GSD update available? + let gsdUpdate = ''; + const cacheFile = path.join(homeDir, '.claude', 'cache', 'gsd-update-check.json'); + if (fs.existsSync(cacheFile)) { + try { + const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); + if (cache.update_available) { + gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ '; + } + } catch (e) {} + } + + // Output + const dirname = path.basename(dir); + if (task) { + process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[1m${task}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`); + } else { + process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`); + } + } catch (e) { + // Silent fail - don't break statusline on parse errors + } +}); diff --git a/hooks/statusline.sh b/hooks/statusline.sh deleted file mode 100755 index 6f0c127e8..000000000 --- a/hooks/statusline.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Claude Code Statusline - GSD Edition -# Shows: model | current task | directory | context usage - -input=$(cat) -model=$(echo "$input" | jq -r '.model.display_name') -dir=$(echo "$input" | jq -r '.workspace.current_dir') -session=$(echo "$input" | jq -r '.session_id') -remaining=$(echo "$input" | jq -r '.context_window.remaining_percentage // empty') - -# Context window display (shows USED percentage) -ctx="" -if [ -n "$remaining" ]; then - rem=$(printf "%.0f" "$remaining") - used=$((100 - rem)) - - # Build progress bar (10 segments) - fills as context is consumed - filled=$((used / 10)) - bar="" - for ((i=0; i/dev/null | head -1) -if [[ -f "$todo" ]]; then - task=$(jq -r '.[] | select(.status=="in_progress") | .activeForm' "$todo" 2>/dev/null | head -1) -fi - -# GSD update available? -gsd_update="" -if [[ -f "$HOME/.claude/cache/gsd-update-check.json" ]]; then - update_available=$(jq -r '.update_available' "$HOME/.claude/cache/gsd-update-check.json" 2>/dev/null) - if [[ "$update_available" == "true" ]]; then - gsd_update=$'\033[33m⬆ /gsd:update\033[0m │ ' - fi -fi - -# Output -dirname=$(basename "$dir") -if [[ -n "$task" ]]; then - printf '%s\033[2m%s\033[0m │ \033[1m%s\033[0m │ \033[2m%s\033[0m%s' "$gsd_update" "$model" "$task" "$dirname" "$ctx" -else - printf '%s\033[2m%s\033[0m │ \033[2m%s\033[0m%s' "$gsd_update" "$model" "$dirname" "$ctx" -fi