mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-30 22:13:28 +08:00
fix: route continuous learning observe hooks through node
This commit is contained in:
committed by
Affaan Mustafa
parent
2006d2ee77
commit
3fadc37802
196
scripts/hooks/observe-runner.js
Normal file
196
scripts/hooks/observe-runner.js
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/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
|
||||
};
|
||||
@@ -137,7 +137,13 @@ async function main() {
|
||||
|
||||
if (hookModule && typeof hookModule.run === 'function') {
|
||||
try {
|
||||
const output = hookModule.run(raw, { truncated, maxStdin: MAX_STDIN });
|
||||
const output = hookModule.run(raw, {
|
||||
hookId,
|
||||
pluginRoot,
|
||||
scriptPath,
|
||||
truncated,
|
||||
maxStdin: MAX_STDIN
|
||||
});
|
||||
process.exit(emitHookResult(raw, output));
|
||||
} catch (runErr) {
|
||||
process.stderr.write(`[Hook] run() error for ${hookId}: ${runErr.message}\n`);
|
||||
@@ -152,6 +158,9 @@ async function main() {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_PLUGIN_ROOT: pluginRoot,
|
||||
ECC_PLUGIN_ROOT: pluginRoot,
|
||||
ECC_HOOK_ID: hookId,
|
||||
ECC_HOOK_INPUT_TRUNCATED: truncated ? '1' : '0',
|
||||
ECC_HOOK_INPUT_MAX_BYTES: String(MAX_STDIN)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user