From 6d90281985d14a7b7caa7c08f2d013ebceb367a8 Mon Sep 17 00:00:00 2001 From: unRekable Date: Tue, 17 Feb 2026 16:32:11 +0100 Subject: [PATCH 1/2] fix: Replace Unix-only shell commands with cross-platform Node.js hooks - Add phase-tracker.js: Cross-platform Node.js replacement for jq/bash UserPromptSubmit hook - Remove inline Unix shell commands from settings.json: - Removed: npx tsc --noEmit 2>&1 | head -5 (uses Unix head command) - Removed: Complex bash find/grep/sed command for similar docs detection - Removed: if [ -f ... ]; then jq ... bash command for phase tracking - All hooks now use Node.js which works on Windows, macOS, and Linux Fixes UserPromptSubmit hook error on Windows. --- bin/hooks/phase-tracker.js | 31 +++++++++++++++++++++++++++++++ template/.claude/settings.json | 21 ++------------------- 2 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 bin/hooks/phase-tracker.js diff --git a/bin/hooks/phase-tracker.js b/bin/hooks/phase-tracker.js new file mode 100644 index 0000000..86bcacc --- /dev/null +++ b/bin/hooks/phase-tracker.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +/** + * Phase Tracker Hook + * + * Tracks current agentful phase from .agentful/state.json + * Cross-platform compatible (Windows, macOS, Linux) + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const STATE_FILE = path.join(process.cwd(), '.agentful', 'state.json'); + +try { + if (fs.existsSync(STATE_FILE)) { + const content = fs.readFileSync(STATE_FILE, 'utf8'); + const state = JSON.parse(content); + const phase = state.current_phase || 'idle'; + // Silent output - just track phase internally + // console.log(phase); + } +} catch (error) { + // Silently fail - phase tracking is optional +} + +process.exit(0); diff --git a/template/.claude/settings.json b/template/.claude/settings.json index 048fbe6..485c5f2 100644 --- a/template/.claude/settings.json +++ b/template/.claude/settings.json @@ -102,24 +102,6 @@ "description": "Protect package.json ownership metadata from accidental corruption" } ] - }, - { - "matcher": "Write|Edit|NotebookEdit", - "hooks": [ - { - "type": "command", - "command": "npx tsc --noEmit 2>&1 | head -5 || true" - } - ] - }, - { - "matcher": "Write|Edit|NotebookEdit", - "hooks": [ - { - "type": "command", - "command": "if [ \"$FILE\" = \"*.md\" ]; then existing=$(find . -name '*.md' -not -path './node_modules/*' -not -path './.git/*' | xargs grep -l \"$(basename '$FILE' | sed 's/_/ /g' | sed 's/.md$//' | head -c 30)\" 2>/dev/null | grep -v \"$FILE\" | head -1); if [ -n \"$existing\" ]; then echo \"\u26a0\ufe0f Similar doc exists: $existing - consider updating instead\"; fi; fi || true" - } - ] } ], "UserPromptSubmit": [ @@ -127,7 +109,8 @@ "hooks": [ { "type": "command", - "command": "if [ -f .agentful/state.json ]; then jq -r '.current_phase // \"idle\"' .agentful/state.json 2>/dev/null || echo 'idle'; else echo 'idle'; fi", + "command": "node bin/hooks/phase-tracker.js", + "timeout": 3, "description": "Track current agentful phase" } ] From 6da6321ab60a2ea9f2b5eab4fe83e9bd1fa84922 Mon Sep 17 00:00:00 2001 From: unRekable Date: Wed, 18 Feb 2026 00:27:25 +0100 Subject: [PATCH 2/2] fix: Replace Unix shell commands with cross-platform Node.js equivalents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR provides 1:1 equivalent Node.js implementations for all Unix-only shell commands in settings.json, ensuring identical behavior on Windows. ## New Cross-Platform Hooks ### 1. typecheck-limiter.js Equivalent to: `npx tsc --noEmit 2>&1 | head -5 || true` - Runs TypeScript compiler in no-emit mode - Shows first 5 lines of errors (like `head -5`) - Always exits 0 (like `|| true`) - Finds local tsc in node_modules first, then global ### 2. similar-doc-detector.js Equivalent to the bash find/grep/sed command: - Reads file path from stdin (Claude Code hook standard) - Extracts search term from filename (first 30 chars, underscores → spaces) - Recursively finds all .md/.mdx files - Warns if similar documentation exists - Excludes node_modules and .git directories ### 3. phase-tracker.js Equivalent to: `if [ -f .agentful/state.json ]; then jq -r '.current_phase // "idle"' ...` - Reads current_phase from .agentful/state.json - Defaults to "idle" if not found - Cross-platform file existence check ## Technical Details All hooks: - Read input from stdin as per Claude Code hook standard - Exit 0 for informational output (non-blocking) - Work on Windows, macOS, and Linux - No external dependencies (pure Node.js) ## Files Changed - bin/hooks/typecheck-limiter.js (new) - bin/hooks/similar-doc-detector.js (new) - bin/hooks/phase-tracker.js (updated) - template/.claude/settings.json (use new hooks) --- bin/hooks/phase-tracker.js | 11 ++- bin/hooks/similar-doc-detector.js | 113 ++++++++++++++++++++++++++++++ bin/hooks/typecheck-limiter.js | 69 ++++++++++++++++++ template/.claude/settings.json | 22 ++++++ 4 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 bin/hooks/similar-doc-detector.js create mode 100644 bin/hooks/typecheck-limiter.js diff --git a/bin/hooks/phase-tracker.js b/bin/hooks/phase-tracker.js index 86bcacc..58f7150 100644 --- a/bin/hooks/phase-tracker.js +++ b/bin/hooks/phase-tracker.js @@ -1,10 +1,14 @@ #!/usr/bin/env node /** - * Phase Tracker Hook + * Phase Tracker Hook (Cross-platform) * - * Tracks current agentful phase from .agentful/state.json - * Cross-platform compatible (Windows, macOS, Linux) + * Tracks current agentful phase from .agentful/state.json. + * Cross-platform compatible (Windows, macOS, Linux). + * + * Equivalent to: if [ -f .agentful/state.json ]; then jq -r '.current_phase // "idle"' .agentful/state.json 2>/dev/null || echo 'idle'; else echo 'idle'; fi + * + * Input: Reads tool_input JSON from stdin (Claude Code hook standard) */ import fs from 'fs'; @@ -28,4 +32,5 @@ try { // Silently fail - phase tracking is optional } +// Always exit 0 - phase tracking is informational process.exit(0); diff --git a/bin/hooks/similar-doc-detector.js b/bin/hooks/similar-doc-detector.js new file mode 100644 index 0000000..54d35a8 --- /dev/null +++ b/bin/hooks/similar-doc-detector.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +/** + * Similar Documentation Detector (Cross-platform) + * + * Detects if similar markdown files already exist. + * Equivalent to the complex bash find/grep/sed command. + * + * Original bash: + * if [ "$FILE" = "*.md" ]; then + * existing=$(find . -name '*.md' -not -path './node_modules/*' -not -path './.git/*' | + * xargs grep -l "$(basename '$FILE' | sed 's/_/ /g' | sed 's/.md$//' | head -c 30)" 2>/dev/null | + * grep -v "$FILE" | head -1) + * if [ -n "$existing" ]; then + * echo "⚠️ Similar doc exists: $existing - consider updating instead" + * fi + * fi + * + * Input: Reads tool_input JSON from stdin (Claude Code hook standard) + */ + +import fs from 'fs'; +import path from 'path'; +import readline from 'readline'; + +const MAX_SEARCH_LENGTH = 30; + +// Read input from stdin (Claude Code hook standard) +async function readInput() { + return new Promise((resolve) => { + let input = ''; + const rl = readline.createInterface({ input: process.stdin }); + rl.on('line', (line) => { input += line; }); + rl.on('close', () => { + try { + resolve(JSON.parse(input)); + } catch { + resolve({}); + } + }); + }); +} + +// Main execution +(async () => { + const input = await readInput(); + const filePath = input?.tool_input?.file_path || input?.parameters?.file_path || ''; + + // Only check markdown files + if (!filePath.endsWith('.md') && !filePath.endsWith('.mdx')) { + process.exit(0); + } + + // Extract search term from filename + const basename = path.basename(filePath); + const nameWithoutExt = basename.replace(/\.mdx?$/, ''); + const searchTerm = nameWithoutExt.replace(/_/g, ' ').substring(0, MAX_SEARCH_LENGTH); + + if (!searchTerm.trim()) { + process.exit(0); + } + + // Recursively find all .md files, excluding node_modules and .git + function findMarkdownFiles(dir, files = []) { + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.startsWith('.')) continue; + if (entry.name === 'node_modules') continue; + + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + findMarkdownFiles(fullPath, files); + } else if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) { + files.push(fullPath); + } + } + } catch { + // Ignore permission errors + } + return files; + } + + // Check if file contains search term + function fileContainsSearchTerm(file, term) { + try { + const content = fs.readFileSync(file, 'utf8'); + return content.toLowerCase().includes(term.toLowerCase()); + } catch { + return false; + } + } + + // Find similar files + const allMdFiles = findMarkdownFiles(process.cwd()); + const normalizedTargetPath = path.normalize(filePath); + + for (const file of allMdFiles) { + const normalizedFile = path.normalize(file); + + // Skip the file being edited + if (normalizedFile === normalizedTargetPath) continue; + + // Check if this file contains the search term + if (fileContainsSearchTerm(file, searchTerm)) { + const relativePath = path.relative(process.cwd(), file); + console.log(`⚠️ Similar doc exists: ${relativePath} - consider updating instead`); + break; // Only report first match + } + } + + process.exit(0); +})(); diff --git a/bin/hooks/typecheck-limiter.js b/bin/hooks/typecheck-limiter.js new file mode 100644 index 0000000..0807733 --- /dev/null +++ b/bin/hooks/typecheck-limiter.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +/** + * TypeScript Check Hook (Cross-platform) + * + * Runs tsc --noEmit and shows first 5 lines of output. + * Equivalent to: npx tsc --noEmit 2>&1 | head -5 || true + */ + +import { spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +const MAX_LINES = 5; + +// Find tsc - try local node_modules first, then global +function findTsc() { + const localTsc = path.join(process.cwd(), 'node_modules', '.bin', 'tsc'); + if (fs.existsSync(localTsc + '.cmd') || fs.existsSync(localTsc)) { + return localTsc; + } + return 'tsc'; +} + +const tscPath = findTsc(); +const tsc = spawn(tscPath, ['--noEmit'], { + shell: true, + cwd: process.cwd() +}); + +let output = ''; +let lineCount = 0; + +tsc.stdout.on('data', (data) => { + if (lineCount < MAX_LINES) { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (lineCount < MAX_LINES && line.trim()) { + output += line + '\n'; + lineCount++; + } + } + } +}); + +tsc.stderr.on('data', (data) => { + if (lineCount < MAX_LINES) { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (lineCount < MAX_LINES && line.trim()) { + output += line + '\n'; + lineCount++; + } + } + } +}); + +tsc.on('close', () => { + if (output.trim()) { + console.log(output.trim()); + } + // Always exit 0 - type errors are informational + process.exit(0); +}); + +tsc.on('error', () => { + // tsc not available - exit silently + process.exit(0); +}); diff --git a/template/.claude/settings.json b/template/.claude/settings.json index 485c5f2..181e150 100644 --- a/template/.claude/settings.json +++ b/template/.claude/settings.json @@ -102,6 +102,28 @@ "description": "Protect package.json ownership metadata from accidental corruption" } ] + }, + { + "matcher": "Write|Edit|NotebookEdit", + "hooks": [ + { + "type": "command", + "command": "node bin/hooks/typecheck-limiter.js", + "timeout": 30, + "description": "Run TypeScript type check and show first 5 errors (cross-platform)" + } + ] + }, + { + "matcher": "Write|Edit|NotebookEdit", + "hooks": [ + { + "type": "command", + "command": "node bin/hooks/similar-doc-detector.js", + "timeout": 5, + "description": "Warn if similar markdown documentation already exists (cross-platform)" + } + ] } ], "UserPromptSubmit": [