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:
Yaojia Wang
2026-03-24 22:26:05 +01:00
commit 2876cca8fe
245 changed files with 54437 additions and 0 deletions

113
hooks/gsd-check-update.js Normal file
View 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();

View 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
View 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
}
});