Files
everything-claude-code/scripts/hooks/observe-runner.js
2026-04-29 21:28:59 -04:00

197 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const OBSERVE_RELATIVE_PATH = path.join('skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
const DEFAULT_TIMEOUT_MS = 9000;
function getPluginRoot(options = {}) {
if (options.pluginRoot && String(options.pluginRoot).trim()) {
return String(options.pluginRoot).trim();
}
if (process.env.CLAUDE_PLUGIN_ROOT && process.env.CLAUDE_PLUGIN_ROOT.trim()) {
return process.env.CLAUDE_PLUGIN_ROOT.trim();
}
if (process.env.ECC_PLUGIN_ROOT && process.env.ECC_PLUGIN_ROOT.trim()) {
return process.env.ECC_PLUGIN_ROOT.trim();
}
return path.resolve(__dirname, '..', '..');
}
function resolveTarget(rootDir, relPath) {
const resolvedRoot = path.resolve(rootDir);
const resolvedTarget = path.resolve(rootDir, relPath);
if (
resolvedTarget !== resolvedRoot &&
!resolvedTarget.startsWith(resolvedRoot + path.sep)
) {
throw new Error(`Path traversal rejected: ${relPath}`);
}
return resolvedTarget;
}
function toShellPath(filePath) {
const normalized = String(filePath || '');
if (process.platform !== 'win32') {
return normalized;
}
return normalized
.replace(/^([A-Za-z]):[\\/]/, (_, driveLetter) => `/${driveLetter.toLowerCase()}/`)
.replace(/\\/g, '/');
}
function findShellBinary() {
const candidates = [];
if (process.env.BASH && process.env.BASH.trim()) {
candidates.push(process.env.BASH.trim());
}
if (process.platform === 'win32') {
candidates.push('bash.exe', 'bash', 'sh');
} else {
candidates.push('bash', 'sh');
}
for (const candidate of candidates) {
const probe = spawnSync(candidate, ['-c', ':'], {
stdio: 'ignore',
windowsHide: true
});
if (!probe.error) {
return candidate;
}
}
return null;
}
function getPhaseFromHookId(hookId) {
const prefix = String(hookId || process.env.ECC_HOOK_ID || '').split(':')[0];
return prefix === 'pre' || prefix === 'post' ? prefix : null;
}
function getTimeoutMs() {
const parsed = Number.parseInt(process.env.ECC_OBSERVE_RUNNER_TIMEOUT_MS || '', 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
}
function combineStderr(stderr, message) {
const prefix = typeof stderr === 'string' && stderr.length > 0
? stderr.endsWith('\n') ? stderr : `${stderr}\n`
: '';
return `${prefix}${message}\n`;
}
function run(raw, options = {}) {
const input = typeof raw === 'string' ? raw : String(raw ?? '');
const phase = getPhaseFromHookId(options.hookId);
if (!phase) {
return {
stderr: '[Hook] observe runner received an unsupported hook id; skipping observation',
exitCode: 0
};
}
const pluginRoot = getPluginRoot(options);
let observePath;
try {
observePath = resolveTarget(pluginRoot, OBSERVE_RELATIVE_PATH);
} catch (error) {
return {
stderr: `[Hook] observe runner path resolution failed: ${error.message}`,
exitCode: 0
};
}
if (!fs.existsSync(observePath)) {
return {
stderr: `[Hook] observe script not found: ${observePath}`,
exitCode: 0
};
}
const shell = findShellBinary();
if (!shell) {
return {
stderr: '[Hook] shell runtime unavailable; skipping continuous-learning observation',
exitCode: 0
};
}
const result = spawnSync(shell, [toShellPath(observePath), phase], {
input,
encoding: 'utf8',
env: {
...process.env,
CLAUDE_PLUGIN_ROOT: pluginRoot,
ECC_PLUGIN_ROOT: pluginRoot
},
cwd: process.cwd(),
timeout: getTimeoutMs(),
windowsHide: true
});
const output = {
exitCode: Number.isInteger(result.status) ? result.status : 0
};
if (typeof result.stdout === 'string' && result.stdout.length > 0) {
output.stdout = result.stdout;
}
if (typeof result.stderr === 'string' && result.stderr.length > 0) {
output.stderr = result.stderr;
}
if (result.error || result.signal || result.status === null) {
const reason = result.error
? result.error.message
: result.signal
? `terminated by signal ${result.signal}`
: 'missing exit status';
output.stderr = combineStderr(output.stderr, `[Hook] observe runner failed: ${reason}`);
output.exitCode = 0;
}
return output;
}
function emitHookResult(raw, output) {
if (output && typeof output === 'object') {
if (output.stderr) {
process.stderr.write(String(output.stderr).endsWith('\n') ? String(output.stderr) : `${output.stderr}\n`);
}
if (Object.prototype.hasOwnProperty.call(output, 'stdout')) {
process.stdout.write(String(output.stdout ?? ''));
} else if (!Number.isInteger(output.exitCode) || output.exitCode === 0) {
process.stdout.write(raw);
}
return Number.isInteger(output.exitCode) ? output.exitCode : 0;
}
process.stdout.write(raw);
return 0;
}
if (require.main === module) {
let raw = '';
try {
raw = fs.readFileSync(0, 'utf8');
} catch (_error) {
raw = '';
}
const output = run(raw, { hookId: process.argv[2] || process.env.ECC_HOOK_ID });
process.exit(emitHookResult(raw, output));
}
module.exports = {
OBSERVE_RELATIVE_PATH,
findShellBinary,
getPhaseFromHookId,
run,
toShellPath
};