chore: initial backup of Claude Code configuration
Includes: CLAUDE.md, settings.json, agents, commands, rules, skills, hooks, contexts, evals, get-shit-done, plugin configs (installed list and marketplace sources). Excludes credentials, runtime caches, telemetry, session data, and plugin binary cache.
This commit is contained in:
113
hooks/gsd-check-update.js
Normal file
113
hooks/gsd-check-update.js
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env node
|
||||
// gsd-hook-version: 1.26.0
|
||||
// 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 { spawn } = require('child_process');
|
||||
|
||||
const homeDir = os.homedir();
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Detect runtime config directory (supports Claude, OpenCode, Gemini)
|
||||
// Respects CLAUDE_CONFIG_DIR for custom config directory setups
|
||||
function detectConfigDir(baseDir) {
|
||||
// Check env override first (supports multi-account setups)
|
||||
const envDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
if (envDir && fs.existsSync(path.join(envDir, 'get-shit-done', 'VERSION'))) {
|
||||
return envDir;
|
||||
}
|
||||
for (const dir of ['.config/opencode', '.opencode', '.gemini', '.claude']) {
|
||||
if (fs.existsSync(path.join(baseDir, dir, 'get-shit-done', 'VERSION'))) {
|
||||
return path.join(baseDir, dir);
|
||||
}
|
||||
}
|
||||
return envDir || path.join(baseDir, '.claude');
|
||||
}
|
||||
|
||||
const globalConfigDir = detectConfigDir(homeDir);
|
||||
const projectConfigDir = detectConfigDir(cwd);
|
||||
const cacheDir = path.join(globalConfigDir, 'cache');
|
||||
const cacheFile = path.join(cacheDir, 'gsd-update-check.json');
|
||||
|
||||
// VERSION file locations (check project first, then global)
|
||||
const projectVersionFile = path.join(projectConfigDir, 'get-shit-done', 'VERSION');
|
||||
const globalVersionFile = path.join(globalConfigDir, 'get-shit-done', 'VERSION');
|
||||
|
||||
// Ensure cache directory exists
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Run check in background (spawn background process, windowsHide prevents console flash)
|
||||
const child = spawn(process.execPath, ['-e', `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const cacheFile = ${JSON.stringify(cacheFile)};
|
||||
const projectVersionFile = ${JSON.stringify(projectVersionFile)};
|
||||
const globalVersionFile = ${JSON.stringify(globalVersionFile)};
|
||||
|
||||
// Check project directory first (local install), then global
|
||||
let installed = '0.0.0';
|
||||
let configDir = '';
|
||||
try {
|
||||
if (fs.existsSync(projectVersionFile)) {
|
||||
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
|
||||
configDir = path.dirname(path.dirname(projectVersionFile));
|
||||
} else if (fs.existsSync(globalVersionFile)) {
|
||||
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
|
||||
configDir = path.dirname(path.dirname(globalVersionFile));
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Check for stale hooks — compare hook version headers against installed VERSION
|
||||
let staleHooks = [];
|
||||
if (configDir) {
|
||||
const hooksDir = path.join(configDir, 'hooks');
|
||||
try {
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js'));
|
||||
for (const hookFile of hookFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
|
||||
const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
|
||||
if (versionMatch) {
|
||||
const hookVersion = versionMatch[1].trim();
|
||||
if (hookVersion !== installed && !hookVersion.includes('{{')) {
|
||||
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
|
||||
}
|
||||
} else {
|
||||
// No version header at all — definitely stale (pre-version-tracking)
|
||||
staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
let latest = null;
|
||||
try {
|
||||
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
|
||||
} catch (e) {}
|
||||
|
||||
const result = {
|
||||
update_available: latest && installed !== latest,
|
||||
installed,
|
||||
latest: latest || 'unknown',
|
||||
checked: Math.floor(Date.now() / 1000),
|
||||
stale_hooks: staleHooks.length > 0 ? staleHooks : undefined
|
||||
};
|
||||
|
||||
fs.writeFileSync(cacheFile, JSON.stringify(result));
|
||||
`], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
detached: true // Required on Windows for proper process detachment
|
||||
});
|
||||
|
||||
child.unref();
|
||||
156
hooks/gsd-context-monitor.js
Normal file
156
hooks/gsd-context-monitor.js
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env node
|
||||
// gsd-hook-version: 1.26.0
|
||||
// Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
|
||||
// Reads context metrics from the statusline bridge file and injects
|
||||
// warnings when context usage is high. This makes the AGENT aware of
|
||||
// context limits (the statusline only shows the user).
|
||||
//
|
||||
// How it works:
|
||||
// 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
|
||||
// 2. This hook reads those metrics after each tool use
|
||||
// 3. When remaining context drops below thresholds, it injects a warning
|
||||
// as additionalContext, which the agent sees in its conversation
|
||||
//
|
||||
// Thresholds:
|
||||
// WARNING (remaining <= 35%): Agent should wrap up current task
|
||||
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
|
||||
//
|
||||
// Debounce: 5 tool uses between warnings to avoid spam
|
||||
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
||||
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
||||
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
||||
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
||||
|
||||
let input = '';
|
||||
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
|
||||
// Windows/Git Bash, or slow Claude Code piping during large outputs),
|
||||
// exit silently instead of hanging until Claude Code kills the process
|
||||
// and reports "hook error". See #775, #1162.
|
||||
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => input += chunk);
|
||||
process.stdin.on('end', () => {
|
||||
clearTimeout(stdinTimeout);
|
||||
try {
|
||||
const data = JSON.parse(input);
|
||||
const sessionId = data.session_id;
|
||||
|
||||
if (!sessionId) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check if context warnings are disabled via config
|
||||
const cwd = data.cwd || process.cwd();
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
if (config.hooks?.context_warnings === false) {
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore config parse errors
|
||||
}
|
||||
}
|
||||
|
||||
const tmpDir = os.tmpdir();
|
||||
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
||||
|
||||
// If no metrics file, this is a subagent or fresh session -- exit silently
|
||||
if (!fs.existsSync(metricsPath)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Ignore stale metrics
|
||||
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const remaining = metrics.remaining_percentage;
|
||||
const usedPct = metrics.used_pct;
|
||||
|
||||
// No warning needed
|
||||
if (remaining > WARNING_THRESHOLD) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Debounce: check if we warned recently
|
||||
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
||||
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
||||
let firstWarn = true;
|
||||
|
||||
if (fs.existsSync(warnPath)) {
|
||||
try {
|
||||
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
|
||||
firstWarn = false;
|
||||
} catch (e) {
|
||||
// Corrupted file, reset
|
||||
}
|
||||
}
|
||||
|
||||
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
||||
|
||||
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
||||
const currentLevel = isCritical ? 'critical' : 'warning';
|
||||
|
||||
// Emit immediately on first warning, then debounce subsequent ones
|
||||
// Severity escalation (WARNING -> CRITICAL) bypasses debounce
|
||||
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
||||
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
||||
// Update counter and exit without warning
|
||||
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Reset debounce counter
|
||||
warnData.callsSinceWarn = 0;
|
||||
warnData.lastLevel = currentLevel;
|
||||
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
||||
|
||||
// Detect if GSD is active (has .planning/STATE.md in working directory)
|
||||
const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
|
||||
|
||||
// Build advisory warning message (never use imperative commands that
|
||||
// override user preferences — see #884)
|
||||
let message;
|
||||
if (isCritical) {
|
||||
message = isGsdActive
|
||||
? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
||||
'Context is nearly exhausted. Do NOT start new complex work or write handoff files — ' +
|
||||
'GSD state is already tracked in STATE.md. Inform the user so they can run ' +
|
||||
'/gsd:pause-work at the next natural stopping point.'
|
||||
: `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
||||
'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
|
||||
'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
|
||||
} else {
|
||||
message = isGsdActive
|
||||
? `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
||||
'Context is getting limited. Avoid starting new complex work. If not between ' +
|
||||
'defined plan steps, inform the user so they can prepare to pause.'
|
||||
: `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
||||
'Be aware that context is getting limited. Avoid unnecessary exploration or ' +
|
||||
'starting new complex work.';
|
||||
}
|
||||
|
||||
const output = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: process.env.GEMINI_API_KEY ? "AfterTool" : "PostToolUse",
|
||||
additionalContext: message
|
||||
}
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(output));
|
||||
} catch (e) {
|
||||
// Silent fail -- never block tool execution
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
119
hooks/gsd-statusline.js
Normal file
119
hooks/gsd-statusline.js
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
// gsd-hook-version: 1.26.0
|
||||
// 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 = '';
|
||||
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
|
||||
// Windows/Git Bash), exit silently instead of hanging. See #775.
|
||||
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => input += chunk);
|
||||
process.stdin.on('end', () => {
|
||||
clearTimeout(stdinTimeout);
|
||||
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 scaled to usable context)
|
||||
// Claude Code reserves ~16.5% for autocompact buffer, so usable context
|
||||
// is 83.5% of the total window. We normalize to show 100% at that point.
|
||||
const AUTO_COMPACT_BUFFER_PCT = 16.5;
|
||||
let ctx = '';
|
||||
if (remaining != null) {
|
||||
// Normalize: subtract buffer from remaining, scale to usable range
|
||||
const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
|
||||
const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
|
||||
|
||||
// Write context metrics to bridge file for the context-monitor PostToolUse hook.
|
||||
// The monitor reads this file to inject agent-facing warnings when context is low.
|
||||
if (session) {
|
||||
try {
|
||||
const bridgePath = path.join(os.tmpdir(), `claude-ctx-${session}.json`);
|
||||
const bridgeData = JSON.stringify({
|
||||
session_id: session,
|
||||
remaining_percentage: remaining,
|
||||
used_pct: used,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
fs.writeFileSync(bridgePath, bridgeData);
|
||||
} catch (e) {
|
||||
// Silent fail -- bridge is best-effort, don't break statusline
|
||||
}
|
||||
}
|
||||
|
||||
// Build progress bar (10 segments)
|
||||
const filled = Math.floor(used / 10);
|
||||
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
||||
|
||||
// Color based on usable context thresholds
|
||||
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();
|
||||
// Respect CLAUDE_CONFIG_DIR for custom config directory setups (#870)
|
||||
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude');
|
||||
const todosDir = path.join(claudeDir, 'todos');
|
||||
if (session && fs.existsSync(todosDir)) {
|
||||
try {
|
||||
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) {}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail on file system errors - don't break statusline
|
||||
}
|
||||
}
|
||||
|
||||
// GSD update available?
|
||||
let gsdUpdate = '';
|
||||
const cacheFile = path.join(claudeDir, '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 │ ';
|
||||
}
|
||||
if (cache.stale_hooks && cache.stale_hooks.length > 0) {
|
||||
gsdUpdate += '\x1b[31m⚠ stale hooks — run /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
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user