UNPKG

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
/** * 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