Files
everything-claude-code/scripts/lib/utils.js
Affaan Mustafa 5c63fa9006 feat: v1.1.0 release - session ID tracking, async hooks, new skills
- Add session ID to session filenames (Issue #62)
- Add getSessionIdShort() helper for unique per-session tracking
- Add async hooks documentation with example
- Create iterative-retrieval skill for progressive context refinement
- Add continuous-learning-v2 skill with instinct-based learning
- Add ecc.tools ecosystem section to README
- Update skills list in README

All 67 tests passing.
2026-01-25 18:21:27 -08:00

398 lines
9.0 KiB
JavaScript

/**
* Cross-platform utility functions for Claude Code hooks and scripts
* Works on Windows, macOS, and Linux
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync, spawnSync } = require('child_process');
// Platform detection
const isWindows = process.platform === 'win32';
const isMacOS = process.platform === 'darwin';
const isLinux = process.platform === 'linux';
/**
* Get the user's home directory (cross-platform)
*/
function getHomeDir() {
return os.homedir();
}
/**
* Get the Claude config directory
*/
function getClaudeDir() {
return path.join(getHomeDir(), '.claude');
}
/**
* Get the sessions directory
*/
function getSessionsDir() {
return path.join(getClaudeDir(), 'sessions');
}
/**
* Get the learned skills directory
*/
function getLearnedSkillsDir() {
return path.join(getClaudeDir(), 'skills', 'learned');
}
/**
* Get the temp directory (cross-platform)
*/
function getTempDir() {
return os.tmpdir();
}
/**
* Ensure a directory exists (create if not)
*/
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
return dirPath;
}
/**
* Get current date in YYYY-MM-DD format
*/
function getDateString() {
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');
return `${year}-${month}-${day}`;
}
/**
* Get current time in HH:MM format
*/
function getTimeString() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* Get short session ID from CLAUDE_SESSION_ID environment variable
* Returns the last 8 characters for uniqueness with brevity
* @param {string} fallback - Fallback value if no session ID (default: 'default')
*/
function getSessionIdShort(fallback = 'default') {
const sessionId = process.env.CLAUDE_SESSION_ID;
if (!sessionId || sessionId.length === 0) {
return fallback;
}
return sessionId.slice(-8);
}
/**
* Get current datetime in YYYY-MM-DD HH:MM:SS format
*/
function getDateTimeString() {
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}`;
}
/**
* Find files matching a pattern in a directory (cross-platform alternative to find)
* @param {string} dir - Directory to search
* @param {string} pattern - File pattern (e.g., "*.tmp", "*.md")
* @param {object} options - Options { maxAge: days, recursive: boolean }
*/
function findFiles(dir, pattern, options = {}) {
const { maxAge = null, recursive = false } = options;
const results = [];
if (!fs.existsSync(dir)) {
return results;
}
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`);
function searchDir(currentDir) {
try {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isFile() && regex.test(entry.name)) {
if (maxAge !== null) {
const stats = fs.statSync(fullPath);
const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24);
if (ageInDays <= maxAge) {
results.push({ path: fullPath, mtime: stats.mtimeMs });
}
} else {
const stats = fs.statSync(fullPath);
results.push({ path: fullPath, mtime: stats.mtimeMs });
}
} else if (entry.isDirectory() && recursive) {
searchDir(fullPath);
}
}
} catch (err) {
// Ignore permission errors
}
}
searchDir(dir);
// Sort by modification time (newest first)
results.sort((a, b) => b.mtime - a.mtime);
return results;
}
/**
* Read JSON from stdin (for hook input)
*/
async function readStdinJson() {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
data += chunk;
});
process.stdin.on('end', () => {
try {
if (data.trim()) {
resolve(JSON.parse(data));
} else {
resolve({});
}
} catch (err) {
reject(err);
}
});
process.stdin.on('error', reject);
});
}
/**
* Log to stderr (visible to user in Claude Code)
*/
function log(message) {
console.error(message);
}
/**
* Output to stdout (returned to Claude)
*/
function output(data) {
if (typeof data === 'object') {
console.log(JSON.stringify(data));
} else {
console.log(data);
}
}
/**
* Read a text file safely
*/
function readFile(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch {
return null;
}
}
/**
* Write a text file
*/
function writeFile(filePath, content) {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, content, 'utf8');
}
/**
* Append to a text file
*/
function appendFile(filePath, content) {
ensureDir(path.dirname(filePath));
fs.appendFileSync(filePath, content, 'utf8');
}
/**
* Check if a command exists in PATH
* Uses execFileSync to prevent command injection
*/
function commandExists(cmd) {
// Validate command name - only allow alphanumeric, dash, underscore, dot
if (!/^[a-zA-Z0-9_.-]+$/.test(cmd)) {
return false;
}
try {
if (isWindows) {
// Use spawnSync to avoid shell interpolation
const result = spawnSync('where', [cmd], { stdio: 'pipe' });
return result.status === 0;
} else {
const result = spawnSync('which', [cmd], { stdio: 'pipe' });
return result.status === 0;
}
} catch {
return false;
}
}
/**
* Run a command and return output
*
* SECURITY NOTE: This function executes shell commands. Only use with
* trusted, hardcoded commands. Never pass user-controlled input directly.
* For user input, use spawnSync with argument arrays instead.
*
* @param {string} cmd - Command to execute (should be trusted/hardcoded)
* @param {object} options - execSync options
*/
function runCommand(cmd, options = {}) {
try {
const result = execSync(cmd, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
...options
});
return { success: true, output: result.trim() };
} catch (err) {
return { success: false, output: err.stderr || err.message };
}
}
/**
* Check if current directory is a git repository
*/
function isGitRepo() {
return runCommand('git rev-parse --git-dir').success;
}
/**
* Get git modified files
*/
function getGitModifiedFiles(patterns = []) {
if (!isGitRepo()) return [];
const result = runCommand('git diff --name-only HEAD');
if (!result.success) return [];
let files = result.output.split('\n').filter(Boolean);
if (patterns.length > 0) {
files = files.filter(file => {
return patterns.some(pattern => {
const regex = new RegExp(pattern);
return regex.test(file);
});
});
}
return files;
}
/**
* Replace text in a file (cross-platform sed alternative)
*/
function replaceInFile(filePath, search, replace) {
const content = readFile(filePath);
if (content === null) return false;
const newContent = content.replace(search, replace);
writeFile(filePath, newContent);
return true;
}
/**
* Count occurrences of a pattern in a file
*/
function countInFile(filePath, pattern) {
const content = readFile(filePath);
if (content === null) return 0;
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'g');
const matches = content.match(regex);
return matches ? matches.length : 0;
}
/**
* Search for pattern in file and return matching lines with line numbers
*/
function grepFile(filePath, pattern) {
const content = readFile(filePath);
if (content === null) return [];
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
const lines = content.split('\n');
const results = [];
lines.forEach((line, index) => {
if (regex.test(line)) {
results.push({ lineNumber: index + 1, content: line });
}
});
return results;
}
module.exports = {
// Platform info
isWindows,
isMacOS,
isLinux,
// Directories
getHomeDir,
getClaudeDir,
getSessionsDir,
getLearnedSkillsDir,
getTempDir,
ensureDir,
// Date/Time
getDateString,
getTimeString,
getDateTimeString,
getSessionIdShort,
// File operations
findFiles,
readFile,
writeFile,
appendFile,
replaceInFile,
countInFile,
grepFile,
// Hook I/O
readStdinJson,
log,
output,
// System
commandExists,
runCommand,
isGitRepo,
getGitModifiedFiles
};