Commit 8c129be2b15a
Changed files (6)
dots
.config
claude
dots/.config/claude/hooks/lib/claude-paths.ts
@@ -1,69 +0,0 @@
-#!/usr/bin/env bun
-
-import { homedir } from 'os';
-import { resolve, join } from 'path';
-import { existsSync } from 'fs';
-
-/**
- * Claude Code infrastructure paths
- *
- * Defaults to ~/.claude but can be overridden with CLAUDE_DIR env var
- */
-export const CLAUDE_DIR = process.env.CLAUDE_DIR
- ? resolve(process.env.CLAUDE_DIR)
- : resolve(homedir(), '.claude');
-
-export const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
-export const SKILLS_DIR = join(CLAUDE_DIR, 'skills');
-export const AGENTS_DIR = join(CLAUDE_DIR, 'agents');
-export const HISTORY_DIR = join(CLAUDE_DIR, 'history');
-
-/**
- * Validate Claude Code directory structure
- * Called automatically on import
- */
-function validateClaudeStructure(): void {
- if (!existsSync(CLAUDE_DIR)) {
- console.error(`❌ CLAUDE_DIR does not exist: ${CLAUDE_DIR}`);
- console.error(` Expected ~/.claude or set CLAUDE_DIR environment variable`);
- process.exit(1);
- }
-
- if (!existsSync(HOOKS_DIR)) {
- console.error(`⚠️ Claude hooks directory not found: ${HOOKS_DIR}`);
- console.error(` This may be expected if hooks haven't been set up yet`);
- // Don't exit - this is okay for initial setup
- }
-}
-
-// Validate on import
-validateClaudeStructure();
-
-/**
- * Get a history file path with proper year-month organization
- * @param subdir - History subdirectory (sessions, learnings, research, etc.)
- * @param filename - File name
- * @returns Full path to history file
- */
-export function getHistoryFilePath(subdir: string, filename: string): string {
- const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, '0');
-
- return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);
-}
-
-/**
- * Get current timestamp in YYYY-MM-DD-HHMMSS format
- */
-export function getTimestamp(): string {
- const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, '0');
- const day = String(now.getDate()).padStart(2, '0');
- const hours = String(now.getHours()).padStart(2, '0');
- const minutes = String(now.getMinutes()).padStart(2, '0');
- const seconds = String(now.getSeconds()).padStart(2, '0');
-
- return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
-}
dots/.config/claude/hooks/capture-tool-output.ts
@@ -1,77 +0,0 @@
-#!/usr/bin/env bun
-
-/**
- * PostToolUse Hook - Captures tool outputs for history tracking
- *
- * Automatically logs interesting tool executions to daily JSONL files
- * for later analysis and session reconstruction.
- *
- * Setup:
- * Add to PostToolUse in settings.json:
- * "PostToolUse": ["bun /home/vincent/.claude/hooks/capture-tool-output.ts"]
- */
-
-import { appendFileSync, mkdirSync, existsSync } from 'fs';
-import { join } from 'path';
-import { CLAUDE_DIR } from './lib/claude-paths';
-
-interface ToolUseData {
- tool_name: string;
- tool_input: Record<string, any>;
- tool_response: Record<string, any>;
- conversation_id: string;
- timestamp: string;
-}
-
-// Configuration - which tools to capture
-const INTERESTING_TOOLS = ['Bash', 'Edit', 'Write', 'Read', 'Task', 'NotebookEdit', 'Skill', 'SlashCommand'];
-
-async function main() {
- try {
- // Read input from stdin
- const input = await Bun.stdin.text();
- if (!input || input.trim() === '') {
- process.exit(0);
- }
-
- const data: ToolUseData = JSON.parse(input);
-
- // Only capture interesting tools
- if (!INTERESTING_TOOLS.includes(data.tool_name)) {
- process.exit(0);
- }
-
- // Get today's date for organization
- const now = new Date();
- const today = now.toISOString().split('T')[0]; // YYYY-MM-DD
- const yearMonth = today.substring(0, 7); // YYYY-MM
-
- // Ensure capture directory exists
- const dateDir = join(CLAUDE_DIR, 'history', 'tool-outputs', yearMonth);
- if (!existsSync(dateDir)) {
- mkdirSync(dateDir, { recursive: true });
- }
-
- // Format output as JSONL (one JSON object per line)
- const captureFile = join(dateDir, `${today}_tool-outputs.jsonl`);
- const captureEntry = JSON.stringify({
- timestamp: data.timestamp || now.toISOString(),
- tool: data.tool_name,
- input: data.tool_input,
- output: data.tool_response,
- session: data.conversation_id
- }) + '\n';
-
- // Append to daily log
- appendFileSync(captureFile, captureEntry);
-
- // Exit successfully (code 0 = continue normally)
- process.exit(0);
- } catch (error) {
- // Silent failure - don't disrupt workflow
- console.error(`[capture-tool-output] Error: ${error}`);
- process.exit(0);
- }
-}
-
-main();
dots/.config/claude/hooks/initialize-session.ts
@@ -1,105 +0,0 @@
-#!/usr/bin/env bun
-
-/**
- * initialize-session.ts
- *
- * Session initialization hook that runs at the start of every Claude Code session.
- *
- * What it does:
- * - Checks if this is a subagent session (skips for subagents)
- * - Sets initial terminal tab title
- * - Logs session start
- *
- * Setup:
- * Add to SessionStart in settings.json:
- * "SessionStart": ["bun /home/vincent/.claude/hooks/initialize-session.ts"]
- */
-
-import { existsSync, mkdirSync, appendFileSync } from 'fs';
-import { join } from 'path';
-import { tmpdir } from 'os';
-import { CLAUDE_DIR, getHistoryFilePath, getTimestamp } from './lib/claude-paths';
-
-// Debounce duration in milliseconds (prevents duplicate SessionStart events)
-const DEBOUNCE_MS = 2000;
-const LOCKFILE = join(tmpdir(), 'claude-session-start.lock');
-
-/**
- * Check if we're within the debounce window to prevent duplicate notifications
- */
-function shouldDebounce(): boolean {
- try {
- if (existsSync(LOCKFILE)) {
- const lockContent = Bun.file(LOCKFILE).text();
- const lockTime = parseInt(await lockContent, 10);
- const now = Date.now();
-
- if (now - lockTime < DEBOUNCE_MS) {
- // Within debounce window, skip this notification
- return true;
- }
- }
-
- // Update lockfile with current timestamp
- await Bun.write(LOCKFILE, Date.now().toString());
- return false;
- } catch (error) {
- // If any error, just proceed (don't break session start)
- try {
- await Bun.write(LOCKFILE, Date.now().toString());
- } catch {}
- return false;
- }
-}
-
-async function main() {
- try {
- // Check if this is a subagent session - if so, exit silently
- const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
- const isSubagent = claudeProjectDir.includes('/.claude/agents/') ||
- process.env.CLAUDE_AGENT_TYPE !== undefined;
-
- if (isSubagent) {
- // This is a subagent session - exit silently without notification
- console.error('🤖 Subagent session detected - skipping session initialization');
- process.exit(0);
- }
-
- // Check debounce to prevent duplicate notifications
- if (await shouldDebounce()) {
- console.error('🔇 Debouncing duplicate SessionStart event');
- process.exit(0);
- }
-
- // Set initial tab title
- const tabTitle = 'Claude Ready';
- process.stderr.write(`\x1b]0;${tabTitle}\x07`);
- process.stderr.write(`\x1b]2;${tabTitle}\x07`);
- process.stderr.write(`\x1b]30;${tabTitle}\x07`);
- console.error(`📍 Session initialized: "${tabTitle}"`);
-
- // Log session start to history (optional)
- const timestamp = getTimestamp();
- const logDir = join(CLAUDE_DIR, 'history', 'sessions', `${timestamp.substring(0, 7)}`);
-
- if (!existsSync(logDir)) {
- mkdirSync(logDir, { recursive: true });
- }
-
- const logEntry = `${new Date().toISOString()} - Session started\n`;
- const logFile = join(logDir, `${timestamp.substring(0, 10)}_session-log.txt`);
-
- try {
- appendFileSync(logFile, logEntry);
- } catch (error) {
- // Silent failure - don't break session start for logging issues
- }
-
- process.exit(0);
- } catch (error) {
- console.error('SessionStart hook error:', error);
- process.exit(1);
- }
-}
-
-main();
dots/.config/claude/hooks/README.md
@@ -1,200 +0,0 @@
-# Claude Code Hooks
-
-This directory contains hooks for Claude Code that automate various tasks and capture session information.
-
-Adapted from [Personal AI Infrastructure](https://github.com/danielmiessler/Personal_AI_Infrastructure).
-
-## Installed Hooks
-
-### initialize-session.ts
-
-**Purpose**: Session initialization
-
-**Triggers**: SessionStart event
-
-**What it does**:
-- Detects and skips subagent sessions
-- Sets terminal tab title to "Claude Ready"
-- Logs session start to history
-- Implements debouncing to prevent duplicate triggers
-
-**Setup**:
-```json
-{
- "hooks": {
- "SessionStart": ["bun ~/.config/claude/hooks/initialize-session.ts"]
- }
-}
-```
-
-### capture-tool-output.ts
-
-**Purpose**: Tool execution logging
-
-**Triggers**: PostToolUse event
-
-**What it does**:
-- Captures outputs from interesting tools (Bash, Edit, Write, Read, Task, etc.)
-- Logs to JSONL files organized by year-month
-- Stores in `~/.config/claude/history/tool-outputs/YYYY-MM/YYYY-MM-DD_tool-outputs.jsonl`
-- Silent failure - doesn't disrupt workflow
-
-**Setup**:
-```json
-{
- "hooks": {
- "PostToolUse": ["bun ~/.config/claude/hooks/capture-tool-output.ts"]
- }
-}
-```
-
-**Captured tools**:
-- Bash
-- Edit, Write, Read
-- Task
-- NotebookEdit
-- Skill, SlashCommand
-
-### validate-docs.ts
-
-**Purpose**: Documentation link validation
-
-**Triggers**: Manual or pre-commit
-
-**What it does**:
-- Scans all markdown files for internal links
-- Checks if linked files exist
-- Reports broken links with file:line numbers
-- Exit code 0 if valid, 1 if broken links found
-
-**Usage**:
-```bash
-# Run manually
-bun ~/.config/claude/hooks/validate-docs.ts
-
-# Add to pre-commit (optional)
-```
-
-## Skipped Hooks
-
-The following hooks from PAI were **not imported** as they require more setup or are not immediately needed:
-
-### capture-session-summary.ts
-**Reason**: Complex - requires parsing conversation output files and generating markdown summaries. Can be added later if needed.
-
-### capture-all-events.ts
-**Reason**: Very comprehensive event logging with agent metadata extraction. More complex than capture-tool-output. Can add if detailed event tracking is needed.
-
-### self-test.ts
-**Reason**: Validates PAI-specific directory structure and configurations. Would need significant adaptation for our setup.
-
-### validate-protected.ts
-**Reason**: Requires `.pai-protected.json` manifest defining protected file patterns. Can add later with custom protection rules.
-
-## Hook Architecture
-
-### Directory Structure
-
-```
-~/.config/claude/hooks/
-├── README.md # This file
-├── lib/
-│ └── claude-paths.ts # Path utilities
-├── initialize-session.ts # Session startup
-├── capture-tool-output.ts # Tool logging
-└── validate-docs.ts # Link validation
-```
-
-### Helper Library: lib/claude-paths.ts
-
-Provides:
-- `CLAUDE_DIR`: Base directory (~/.config/claude)
-- `HOOKS_DIR`, `SKILLS_DIR`, `AGENTS_DIR`, `HISTORY_DIR`: Subdirectories
-- `getHistoryFilePath(subdir, filename)`: Generate history file paths
-- `getTimestamp()`: Get formatted timestamp
-- Path validation on import
-
-**Note**: For backward compatibility, `~/.claude/` is symlinked to `~/.config/claude/`.
-
-## Configuration
-
-Hooks are configured in Claude Code settings (`.config/claude/settings.json` or `.config/claude/settings.local.json`):
-
-```json
-{
- "hooks": {
- "SessionStart": ["bun ~/.config/claude/hooks/initialize-session.ts"],
- "PostToolUse": ["bun ~/.config/claude/hooks/capture-tool-output.ts"]
- }
-}
-```
-
-Available hook events:
-- `SessionStart`: Session begins
-- `SessionEnd`: Session ends
-- `PreToolUse`: Before tool execution
-- `PostToolUse`: After tool execution
-- `UserPromptSubmit`: After user submits prompt
-- `Stop`: Session stopped
-- `SubagentStop`: Subagent stopped
-- `PreCompact`: Before context compaction
-- `Notification`: System notification
-
-## Requirements
-
-- **Bun**: All hooks use `#!/usr/bin/env bun` shebang
-- Install: `curl -fsSL https://bun.sh/install | bash`
-- Already installed on kyushu system
-
-## History Logging
-
-Hooks that capture data store it in `~/.config/claude/history/`:
-
-```
-~/.config/claude/history/
-├── sessions/
-│ └── YYYY-MM/
-│ └── YYYY-MM-DD_session-log.txt
-└── tool-outputs/
- └── YYYY-MM/
- └── YYYY-MM-DD_tool-outputs.jsonl
-```
-
-This integrates with the history system documented in `.config/claude/skills/CORE/history-system.md`.
-
-## Adding More Hooks
-
-To add additional hooks:
-
-1. Create the hook file in `~/.config/claude/hooks/`
-2. Add shebang: `#!/usr/bin/env bun`
-3. Make it executable: `chmod +x hook-name.ts`
-4. Use `lib/claude-paths.ts` for paths
-5. Add to settings.json under appropriate event
-6. Test manually before enabling
-
-## Troubleshooting
-
-**Hook not running:**
-- Check settings.json syntax
-- Verify file is executable (`ls -la ~/.config/claude/hooks/`)
-- Check hook output in stderr
-- Verify Bun is installed: `bun --version`
-
-**Permission errors:**
-- Ensure directories exist: `mkdir -p ~/.config/claude/history/{sessions,tool-outputs}`
-- Check write permissions
-
-**Debugging:**
-- Hooks write to stderr - check terminal output
-- Add debug logging: `console.error('[hook-name] Debug message')`
-- Run manually: `bun ~/.config/claude/hooks/hook-name.ts`
-
-## Future Enhancements
-
-Consider adding:
-- Session summary generation (port capture-session-summary.ts)
-- Protected file validation (port validate-protected.ts)
-- Custom event logging for specific workflows
-- Integration with external notification systems
-- Automatic documentation generation
dots/.config/claude/hooks/validate-docs.ts
@@ -1,155 +0,0 @@
-#!/usr/bin/env bun
-
-/**
- * Documentation Link Validator
- *
- * Validates that all internal markdown links point to existing files.
- * Useful as a pre-commit hook to prevent broken documentation.
- *
- * Usage:
- * bun run ~/.claude/hooks/validate-docs.ts
- *
- * Exit codes:
- * 0 - All links valid
- * 1 - Broken links found
- */
-
-import { readFileSync, existsSync } from 'fs';
-import { join, dirname, resolve } from 'path';
-import { Glob } from 'bun';
-
-// ANSI color codes
-const colors = {
- reset: '\x1b[0m',
- red: '\x1b[31m',
- green: '\x1b[32m',
- yellow: '\x1b[33m',
- cyan: '\x1b[36m',
-};
-
-interface BrokenLink {
- file: string;
- link: string;
- target: string;
- line: number;
-}
-
-/**
- * Extract markdown links from content
- */
-function extractLinks(content: string): { link: string; line: number }[] {
- const links: { link: string; line: number }[] = [];
- const lines = content.split('\n');
-
- // Match [text](path) style links
- const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
-
- lines.forEach((line, index) => {
- let match;
- while ((match = linkRegex.exec(line)) !== null) {
- const link = match[2];
-
- // Skip external URLs, anchors, and mailto links
- if (link.startsWith('http://') ||
- link.startsWith('https://') ||
- link.startsWith('#') ||
- link.startsWith('mailto:')) {
- continue;
- }
-
- links.push({ link, line: index + 1 });
- }
- });
-
- return links;
-}
-
-/**
- * Resolve a link path relative to the file
- */
-function resolveLink(fromFile: string, linkPath: string, baseDir: string): string {
- // Remove anchor if present
- const pathWithoutAnchor = linkPath.split('#')[0];
-
- // If it starts with ~, expand to home directory
- if (pathWithoutAnchor.startsWith('~/')) {
- return resolve(process.env.HOME || '', pathWithoutAnchor.substring(2));
- }
-
- // If it's absolute, use as-is
- if (pathWithoutAnchor.startsWith('/')) {
- return pathWithoutAnchor;
- }
-
- // Otherwise, resolve relative to the file's directory
- const fileDir = dirname(fromFile);
- return resolve(fileDir, pathWithoutAnchor);
-}
-
-/**
- * Validate markdown files in a directory
- */
-function validateDocs(baseDir: string): BrokenLink[] {
- const brokenLinks: BrokenLink[] = [];
-
- // Find all markdown files
- const glob = new Glob('**/*.md');
-
- for (const file of glob.scanSync({ cwd: baseDir, absolute: false })) {
- const filePath = join(baseDir, file);
-
- // Skip node_modules and hidden directories
- if (filePath.includes('node_modules') || filePath.includes('/.')) {
- continue;
- }
-
- try {
- const content = readFileSync(filePath, 'utf-8');
- const links = extractLinks(content);
-
- for (const { link, line } of links) {
- const targetPath = resolveLink(filePath, link, baseDir);
-
- // Check if target exists
- if (!existsSync(targetPath)) {
- brokenLinks.push({
- file: file,
- link: link,
- target: targetPath,
- line: line,
- });
- }
- }
- } catch (error) {
- console.error(`${colors.yellow}Warning: Could not read ${file}${colors.reset}`);
- }
- }
-
- return brokenLinks;
-}
-
-function main(): number {
- const baseDir = process.cwd();
-
- console.log(`\n${colors.cyan}🔍 Documentation Link Validator${colors.reset}`);
- console.log(`${colors.cyan} Base directory: ${baseDir}${colors.reset}\n`);
-
- const brokenLinks = validateDocs(baseDir);
-
- if (brokenLinks.length > 0) {
- console.log(`\n${colors.red}❌ Found ${brokenLinks.length} broken link(s):${colors.reset}\n`);
-
- for (const { file, link, line } of brokenLinks) {
- console.log(` ${colors.yellow}${file}:${line}${colors.reset}`);
- console.log(` → ${colors.red}${link}${colors.reset} (not found)\n`);
- }
-
- console.log(`\n${colors.red}Documentation validation failed. Please fix the broken links.${colors.reset}\n`);
- return 1;
- }
-
- console.log(`${colors.green}✅ All documentation links are valid${colors.reset}\n`);
- return 0;
-}
-
-process.exit(main());
dots/Makefile
@@ -22,22 +22,11 @@ niri : ~/.config/niri/config.kdl
all += claude-skills
claude-skills : ~/.config/claude/skills
-all += claude-agents
+all += claude-agents claude-settings claude-plugins claude-statusline claude-compat
claude-agents : ~/.config/claude/agents
-
-all += claude-hooks
-claude-hooks : ~/.config/claude/hooks
-
-all += claude-settings
claude-settings : ~/.config/claude/settings.json
-
-all += claude-plugins
claude-plugins : ~/.config/claude/plugins/session-manager
-
-all += claude-statusline
claude-statusline : ~/.config/claude/statusline.sh
-
-all += claude-compat
claude-compat : ~/.claude
# Backward compatibility: symlink ~/.claude to ~/.config/claude