aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
116 lines • 4.26 kB
JavaScript
/**
* Canonical hook source loader.
*
* Reads HookSource YAML/JSON files from a known directory layout. Per the
* orchestrator wiring in `src/cli/handlers/use.ts`, the operator opts in to
* cross-provider hook translation via `--enable-cross-provider-hooks`; when
* the flag is set, this loader gathers the canonical source files and the
* `bridgeAll()` API translates them.
*
* Source location convention: `agentic/code/addons/aiwg-hooks/canonical/*.yaml`
* Each file declares one HookSource per the schema in `types.ts`.
*
* The loader is forgiving: missing directory yields an empty source list
* (not an error) so operators can opt in to the flag without authoring
* any sources first.
*/
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import * as yaml from 'js-yaml';
const VALID_EVENTS = new Set([
'PreToolUse',
'PostToolUse',
'UserPromptSubmit',
'SessionStart',
'SessionEnd',
'Stop',
]);
/**
* Validate and normalize a parsed HookSource. Returns null if the input
* doesn't satisfy the minimal schema. Caller logs the rejection.
*/
function normalize(input, filePath) {
if (!input || typeof input !== 'object') {
return { source: null, error: `${filePath}: not an object` };
}
const o = input;
if (typeof o.id !== 'string' || !o.id) {
return { source: null, error: `${filePath}: missing or non-string \`id\`` };
}
if (typeof o.command !== 'string' || !o.command) {
return { source: null, error: `${filePath}: missing or non-string \`command\`` };
}
const events = Array.isArray(o.events) ? o.events.filter((e) => typeof e === 'string' && VALID_EVENTS.has(e)) : [];
if (events.length === 0) {
return { source: null, error: `${filePath}: \`events\` must be a non-empty array of valid event names` };
}
const source = {
id: o.id,
description: typeof o.description === 'string' ? o.description : '',
events,
command: o.command,
args: Array.isArray(o.args) ? o.args.filter((a) => typeof a === 'string') : undefined,
safetyCritical: o['safety-critical'] === true || o.safetyCritical === true,
degradeOn: Array.isArray(o['degrade-on'])
? o['degrade-on'].filter((p) => typeof p === 'string')
: Array.isArray(o.degradeOn)
? o.degradeOn.filter((p) => typeof p === 'string')
: undefined,
workingDir: typeof o['working-dir'] === 'string'
? o['working-dir']
: typeof o.workingDir === 'string'
? o.workingDir
: undefined,
};
return { source };
}
/**
* Load all canonical hook sources from `<frameworkRoot>/agentic/code/addons/
* aiwg-hooks/canonical/`. Returns the parsed sources plus any rejections so
* the orchestrator can warn operators about malformed files.
*/
export async function loadHookSources(frameworkRoot) {
const dir = path.join(frameworkRoot, 'agentic', 'code', 'addons', 'aiwg-hooks', 'canonical');
const sources = [];
const errors = [];
let entries;
try {
entries = await fs.readdir(dir);
}
catch (err) {
if (err.code === 'ENOENT') {
return { sources, errors };
}
throw err;
}
for (const entry of entries) {
if (!/\.(yaml|yml|json)$/i.test(entry))
continue;
const filePath = path.join(dir, entry);
let raw;
try {
raw = await fs.readFile(filePath, 'utf8');
}
catch (err) {
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
continue;
}
let parsed;
try {
parsed = entry.endsWith('.json') ? JSON.parse(raw) : yaml.load(raw);
}
catch (err) {
errors.push(`${filePath}: parse error — ${err instanceof Error ? err.message : String(err)}`);
continue;
}
const { source, error } = normalize(parsed, filePath);
if (error) {
errors.push(error);
continue;
}
if (source)
sources.push(source);
}
return { sources, errors };
}
//# sourceMappingURL=loader.js.map