eaba21329c
* refactor: Clean up hook-response and new-hook files - Removed 'PreCompact' hook type and associated logic from hook-response.ts for improved type safety. - Deleted extensive architecture comments in new-hook.ts to streamline code readability. - Simplified debug logging in new-hook.ts to reduce verbosity. - Enhanced ensureWorkerRunning function in worker-utils.ts with a final health check before throwing errors. - Added a new documentation file outlining the hooks cleanup process and future improvements. * Refactor cleanup and user message hooks - Updated cleanup-hook.js to improve error handling and remove unnecessary input checks. - Simplified user-message-hook.js by removing time-sensitive announcements and streamlining output. - Enhanced logging functionality in both hooks for better debugging and clarity. * Refactor error handling in hooks to use centralized error handler - Introduced `handleWorkerError` function in `src/shared/hook-error-handler.ts` to manage worker-related errors. - Updated `context-hook.ts`, `new-hook.ts`, `save-hook.ts`, and `summary-hook.ts` to utilize the new error handler, simplifying error management and improving code readability. - Removed repetitive error handling logic from individual hooks, ensuring consistent user-friendly messages for connection issues. * Refactor user-message and summary hooks to utilize shared transcript parser; introduce hook exit codes - Moved user message extraction logic to a new shared module `transcript-parser.ts` for better code reuse. - Updated `summary-hook.ts` to use the new `extractLastMessage` function for retrieving user and assistant messages. - Replaced direct exit code usage in `user-message-hook.ts` with constants from `hook-constants.ts` for improved readability and maintainability. - Added `HOOK_EXIT_CODES` to `hook-constants.ts` to standardize exit codes across hooks. * Refactor hook input interfaces to enforce required fields - Updated `SessionStartInput`, `UserPromptSubmitInput`, `PostToolUseInput`, and `StopInput` interfaces to require `session_id`, `transcript_path`, and `cwd` fields, ensuring better type safety and clarity in hook inputs. - Removed optional index signatures from these interfaces to prevent unintended properties and improve code maintainability. - Adjusted related hook implementations to align with the new interface definitions. * Refactor save-hook to remove tool skipping logic; enhance summary-hook to handle spinner stopping with error logging; update SessionRoutes to load skip tools from settings; add CLAUDE_MEM_SKIP_TOOLS to SettingsDefaultsManager for configurable tool exclusion. * Document CLAUDE_MEM_SKIP_TOOLS setting in public docs Added documentation for the new CLAUDE_MEM_SKIP_TOOLS configuration setting in response to PR review feedback. Users can now discover and customize which tools are excluded from observations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
import path from "path";
|
|
import { existsSync } from "fs";
|
|
import { homedir } from "os";
|
|
import { spawnSync } from "child_process";
|
|
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
|
|
import { logger } from "../utils/logger.js";
|
|
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
|
|
|
|
// CRITICAL: Always use marketplace directory for PM2/ecosystem
|
|
// This ensures cross-platform compatibility and avoids cache directory confusion
|
|
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
|
|
|
// Named constants for health checks
|
|
// Windows needs longer timeouts due to startup overhead
|
|
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
|
|
const WORKER_STARTUP_WAIT_MS = HOOK_TIMEOUTS.WORKER_STARTUP_WAIT;
|
|
const WORKER_STARTUP_RETRIES = HOOK_TIMEOUTS.WORKER_STARTUP_RETRIES;
|
|
|
|
/**
|
|
* Get the worker port number
|
|
* Priority: ~/.claude-mem/settings.json > env var > default
|
|
*/
|
|
export function getWorkerPort(): number {
|
|
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
|
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
|
return parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
|
}
|
|
|
|
/**
|
|
* Check if worker is responsive by trying the health endpoint
|
|
*/
|
|
async function isWorkerHealthy(): Promise<boolean> {
|
|
try {
|
|
const port = getWorkerPort();
|
|
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
logger.debug('SYSTEM', 'Worker health check failed', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
errorType: error?.constructor?.name
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the worker service
|
|
* On Windows: Uses PowerShell Start-Process with hidden window to avoid console flash
|
|
* On Unix: Uses PM2 for process management
|
|
*/
|
|
async function startWorker(): Promise<boolean> {
|
|
try {
|
|
const workerScript = path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs');
|
|
|
|
if (!existsSync(workerScript)) {
|
|
throw new Error(`Worker script not found at ${workerScript}`);
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
// On Windows, use PowerShell Start-Process with -WindowStyle Hidden
|
|
// This avoids visible console windows that PM2 creates on Windows
|
|
// Escape single quotes for PowerShell by doubling them
|
|
const escapedScript = workerScript.replace(/'/g, "''");
|
|
const escapedWorkingDir = MARKETPLACE_ROOT.replace(/'/g, "''");
|
|
|
|
const result = spawnSync('powershell.exe', [
|
|
'-NoProfile',
|
|
'-NonInteractive',
|
|
'-Command',
|
|
`Start-Process -FilePath 'node' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkingDir}' -WindowStyle Hidden`
|
|
], {
|
|
cwd: MARKETPLACE_ROOT,
|
|
stdio: 'pipe',
|
|
encoding: 'utf-8',
|
|
windowsHide: true
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr || 'PowerShell Start-Process failed');
|
|
}
|
|
} else {
|
|
// On Unix, use PM2 for process management
|
|
const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs');
|
|
|
|
if (!existsSync(ecosystemPath)) {
|
|
throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
|
|
}
|
|
|
|
const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
|
|
let pm2Command: string;
|
|
|
|
if (existsSync(localPm2Base)) {
|
|
pm2Command = localPm2Base;
|
|
} else {
|
|
// Check if global pm2 exists
|
|
const globalPm2Check = spawnSync('which', ['pm2'], {
|
|
encoding: 'utf-8',
|
|
stdio: 'pipe'
|
|
});
|
|
|
|
if (globalPm2Check.status !== 0) {
|
|
throw new Error(
|
|
'PM2 not found. Install it locally with:\n' +
|
|
` cd ${MARKETPLACE_ROOT}\n` +
|
|
' npm install\n\n' +
|
|
'Or install globally with: npm install -g pm2'
|
|
);
|
|
}
|
|
|
|
pm2Command = 'pm2';
|
|
}
|
|
|
|
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
|
|
cwd: MARKETPLACE_ROOT,
|
|
stdio: 'pipe',
|
|
encoding: 'utf-8'
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr || 'PM2 start failed');
|
|
}
|
|
}
|
|
|
|
// Wait for worker to become healthy
|
|
for (let i = 0; i < WORKER_STARTUP_RETRIES; i++) {
|
|
await new Promise(resolve => setTimeout(resolve, WORKER_STARTUP_WAIT_MS));
|
|
if (await isWorkerHealthy()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
logger.error('SYSTEM', 'Failed to start worker', {
|
|
platform: process.platform,
|
|
workerScript: path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'),
|
|
error: error instanceof Error ? error.message : String(error),
|
|
marketplaceRoot: MARKETPLACE_ROOT
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure worker service is running
|
|
* Checks health and auto-starts if not running
|
|
*/
|
|
export async function ensureWorkerRunning(): Promise<void> {
|
|
// Check if already healthy
|
|
if (await isWorkerHealthy()) {
|
|
return;
|
|
}
|
|
|
|
// Try to start the worker
|
|
const started = await startWorker();
|
|
|
|
// Final health check before throwing error
|
|
// Worker might be already running but was temporarily unresponsive
|
|
if (!started && await isWorkerHealthy()) {
|
|
return;
|
|
}
|
|
|
|
if (!started) {
|
|
const port = getWorkerPort();
|
|
throw new Error(
|
|
`Worker service failed to start on port ${port}.\n\n` +
|
|
`To start manually, run:\n` +
|
|
` cd ${MARKETPLACE_ROOT}\n` +
|
|
` npx pm2 start ecosystem.config.cjs\n\n` +
|
|
`If already running, try: npx pm2 restart claude-mem-worker`
|
|
);
|
|
}
|
|
}
|