Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions bin/hooks/phase-tracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node

/**
* Phase Tracker Hook (Cross-platform)
*
* 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';
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
}

// Always exit 0 - phase tracking is informational
process.exit(0);
113 changes: 113 additions & 0 deletions bin/hooks/similar-doc-detector.js
Original file line number Diff line number Diff line change
@@ -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);
})();
69 changes: 69 additions & 0 deletions bin/hooks/typecheck-limiter.js
Original file line number Diff line number Diff line change
@@ -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);
});
11 changes: 8 additions & 3 deletions template/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@
"hooks": [
{
"type": "command",
"command": "npx tsc --noEmit 2>&1 | head -5 || true"
"command": "node bin/hooks/typecheck-limiter.js",
"timeout": 30,
"description": "Run TypeScript type check and show first 5 errors (cross-platform)"
}
]
},
Expand All @@ -117,7 +119,9 @@
"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"
"command": "node bin/hooks/similar-doc-detector.js",
"timeout": 5,
"description": "Warn if similar markdown documentation already exists (cross-platform)"
}
]
}
Expand All @@ -127,7 +131,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"
}
]
Expand Down
Loading