89 lines
3.7 KiB
JavaScript
89 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
||
// UserPromptSubmit hook: records an explicit "Core-Freigabe: <area>[, <area>]" from the user's message.
|
||
// The release covers only this message; every new message replaces or removes it.
|
||
// Only the user's own prompt is read, never tool output or file content.
|
||
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { RELEASE_FILE, ALL_AREAS, resolveAreaName } from './core-areas.mjs';
|
||
|
||
const RELEASE_PHRASE = /core[- ]?freigabe\s*:?\s*/gi;
|
||
const SEPARATOR = /^(\s*(,|;|\/|&|\+|\bund\b|\band\b)\s*|\s+)/i;
|
||
|
||
// Reads area names from the start of the text after the phrase; trailing prose ends the list.
|
||
// Any character other than a letter or space (dash, colon, digit, punctuation, newline) ends a name,
|
||
// so "Core-Freigabe: Shared Kernel – füge ..." releases Shared Kernel.
|
||
function readAreas(tail, unknown) {
|
||
const areas = [];
|
||
let rest = tail;
|
||
while (rest.length > 0) {
|
||
const word = rest.match(/^[\p{L}][\p{L} ]*?(?=$|[^\p{L} ]|\s+(und|and)\b)/u)?.[0] ?? '';
|
||
const candidates = word.split(' ').map((_, index, parts) => parts.slice(0, parts.length - index).join(' '));
|
||
const match = candidates.find((candidate) => resolveAreaName(candidate));
|
||
if (!match) {
|
||
if (areas.length === 0 && word) unknown.push(word.trim());
|
||
break;
|
||
}
|
||
areas.push(resolveAreaName(match));
|
||
rest = rest.slice(match.length);
|
||
const separator = rest.match(SEPARATOR);
|
||
if (!separator) break;
|
||
rest = rest.slice(separator[0].length);
|
||
}
|
||
return areas;
|
||
}
|
||
|
||
function parseRelease(prompt) {
|
||
const areas = new Set();
|
||
const unknown = [];
|
||
for (const match of prompt.matchAll(RELEASE_PHRASE)) {
|
||
const tail = prompt.slice(match.index + match[0].length);
|
||
for (const area of readAreas(tail, unknown)) areas.add(area);
|
||
}
|
||
return { areas: [...areas], unknown };
|
||
}
|
||
|
||
function label(area) {
|
||
return area === ALL_AREAS ? 'alle Core-Bereiche ausser Governance' : area;
|
||
}
|
||
|
||
function main(raw) {
|
||
const input = JSON.parse(raw);
|
||
const projectDir = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
|
||
const releaseFile = path.join(projectDir, RELEASE_FILE);
|
||
const { areas, unknown } = parseRelease(String(input.prompt ?? ''));
|
||
fs.rmSync(releaseFile, { force: true });
|
||
const output = {};
|
||
if (areas.length > 0) {
|
||
fs.writeFileSync(releaseFile, JSON.stringify({ sessionId: input.session_id ?? null, areas, createdAt: new Date().toISOString() }));
|
||
const text = areas.map(label).join(', ');
|
||
output.systemMessage = `✅ CORE-FREIGABE für diese Nachricht: ${text}`;
|
||
output.hookSpecificOutput = {
|
||
hookEventName: 'UserPromptSubmit',
|
||
additionalContext: `The user released these Core areas for this message only: ${text}. Other Core areas stay blocked.`,
|
||
};
|
||
}
|
||
if (unknown.length > 0) {
|
||
output.systemMessage = [output.systemMessage, `⚠ Core-Freigabe nicht erkannt: "${unknown.join('", "')}". Gueltige Bereiche: Ingest, Normalization, Evaluation, Store, Disclosure, Reference Catalogs, Shared Kernel, Presentation, Platform, Core Tests, Governance, alle.`]
|
||
.filter(Boolean).join('\n');
|
||
}
|
||
if (Object.keys(output).length > 0) process.stdout.write(JSON.stringify(output));
|
||
}
|
||
|
||
let raw = '';
|
||
process.stdin.setEncoding('utf8');
|
||
process.stdin.on('data', (chunk) => { raw += chunk; });
|
||
process.stdin.on('end', () => {
|
||
try {
|
||
main(raw);
|
||
} catch (error) {
|
||
// Fail closed: without a readable message no release remains active.
|
||
try {
|
||
fs.rmSync(path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), RELEASE_FILE), { force: true });
|
||
} catch {}
|
||
process.stdout.write(JSON.stringify({
|
||
systemMessage: `⚠ Core-Freigabe konnte nicht gelesen werden (${error instanceof Error ? error.message : 'unbekannter Fehler'}). Der Core bleibt gesperrt.`,
|
||
}));
|
||
}
|
||
});
|