mirror of
https://github.com/davila7/claude-code-templates.git
synced 2026-09-19 01:30:23 +08:00
0281438fc6
* feat: add Function Hooks component type (experimental) with 10 hooks and blog Introduce "function-hooks" as a new first-class component type based on the Anthropic proposal in anthropics/claude-code#91870 (TypeScript hooks as Koa-style middleware on a $ engine interface). The feature is not shipped; every API name is provisional and the section is labelled experimental everywhere. - 10 function hooks under cli-tool/components/function-hooks/ (security, productivity, observability, ui, integrations, enterprise), each as a .md doc page plus the installable .ts/.tsx hooks-module - CLI: --function-hook installs the module as a local plugin under .claude/plugins/<name>/ (plugin.json, hooks/hooks.json with modules) and prints the experimental warning - Catalog generator: scan function-hooks and emit dashboard artifacts - Dashboard: type wiring, /function-hooks page with experimental banner linking to the issue, Events/Module cards on the detail page, sitemap, cart, webmcp; track-download now accepts loop/function-hook - Blog: "Claude Code Function Hooks (Experimental)" article with cover - Docs: CLAUDE.md and cli-tool rule Claude-Session: https://claude.ai/code/session_01YBoJbmNMcRCexuKpH9mQtW * fix: store function hooks as hooks.json + module, install as skills-dir plugin Function hooks are not markdown docs. Per the proposal's architecture doc, a function hook is a plugin's hooks/hooks.json with a "modules" key naming a .ts/.tsx hooks-module beside it. Align the catalog and the CLI with that: - Components are now {name}.json (hooks.json + catalog description) plus the {name}.ts/.tsx module it names, mirroring how shell hooks store .json + .py/.sh. The .md docs are removed. - Generator reads the named module into the per-component content file (module, moduleSource); the detail page renders the JSON and the module with the experimental notice. - CLI --function-hook downloads hooks.json and every module it names, strips description, and writes the plugin to .claude/skills/{name}/, which Claude Code auto-loads as {name}@skills-dir per the plugins reference (no --plugin-dir needed). - Docs and blog updated to the same install path. Claude-Session: https://claude.ai/code/session_01YBoJbmNMcRCexuKpH9mQtW * fix(function-hooks): address review findings - CartSidebar: add --loop and --function-hook to TYPE_FLAGS so stack commands install them - SendToRepoModal: export loops and function hooks (plugin layout with hooks.json + module) - CLI: validate function hook identifier and hooks-module file names before building URLs/paths - block-destructive-commands: rm rule now catches split flags, --recursive, quoted/trailing-slash targets, --no-preserve-root - admin-capability-lockdown: shellPolicy deny|guardrail|allow; default withholds Bash, guardrail is labelled bypassable - regenerate catalog Claude-Session: https://claude.ai/code/session_01WsWgpvgeJgRMoLiYDcRXMU
51 lines
2.4 KiB
TypeScript
51 lines
2.4 KiB
TypeScript
/**
|
|
* npm-to-pnpm-rewriter — Function Hook (EXPERIMENTAL)
|
|
*
|
|
* Rewrites npm / npx invocations to the package manager your project uses
|
|
* (pnpm by default, yarn or bun via options). Placement: "modifying" —
|
|
* the hook forwards a copy of the event with a changed command.
|
|
*
|
|
* Function hooks are an Anthropic proposal under community review:
|
|
* https://github.com/anthropics/claude-code/issues/91870
|
|
* Every API name below is provisional.
|
|
*/
|
|
|
|
type Engine = any;
|
|
type Next = ((e: any) => Promise<any>) & { event: string; origin: string; signal: AbortSignal };
|
|
|
|
type Manager = "pnpm" | "yarn" | "bun";
|
|
|
|
// [regex on the npm form, replacement per manager]. $1 keeps the command separator.
|
|
const REWRITES: Array<[RegExp, Record<Manager, string>]> = [
|
|
[/(^|&&\s*|;\s*|\|\s*)npm\s+ci\b/g, { pnpm: "$1pnpm install --frozen-lockfile", yarn: "$1yarn install --immutable", bun: "$1bun install --frozen-lockfile" }],
|
|
[/(^|&&\s*|;\s*|\|\s*)npm\s+(install|i|add)\b/g, { pnpm: "$1pnpm add", yarn: "$1yarn add", bun: "$1bun add" }],
|
|
[/(^|&&\s*|;\s*|\|\s*)npm\s+(uninstall|remove|rm)\b/g, { pnpm: "$1pnpm remove", yarn: "$1yarn remove", bun: "$1bun remove" }],
|
|
[/(^|&&\s*|;\s*|\|\s*)npm\s+run\b/g, { pnpm: "$1pnpm run", yarn: "$1yarn run", bun: "$1bun run" }],
|
|
[/(^|&&\s*|;\s*|\|\s*)npm\s+(test|start|build)\b/g, { pnpm: "$1pnpm $2", yarn: "$1yarn $2", bun: "$1bun run $2" }],
|
|
[/(^|&&\s*|;\s*|\|\s*)npx\s+/g, { pnpm: "$1pnpm dlx ", yarn: "$1yarn dlx ", bun: "$1bunx " }],
|
|
];
|
|
|
|
// "<manager> add" with no package means "install everything": keep the bare install form.
|
|
function fixBareAdd(command: string, manager: Manager): string {
|
|
return command.replace(new RegExp(`${manager} add(\\s*(&&|;|\\||$))`, "g"), `${manager} install$1`);
|
|
}
|
|
|
|
export function register(on: any, options: Record<string, any> = {}) {
|
|
const manager: Manager = options.manager ?? "pnpm";
|
|
|
|
on("tool.call", { tool: "Bash" }, ($: Engine, e: any, next: Next) => {
|
|
const original: string = e.command ?? "";
|
|
if (!/\bnp[mx]\b/.test(original)) return next(e);
|
|
|
|
let rewritten = original;
|
|
for (const [re, byManager] of REWRITES) rewritten = rewritten.replace(re, byManager[manager]);
|
|
rewritten = fixBareAdd(rewritten, manager);
|
|
|
|
if (rewritten === original) return next(e);
|
|
|
|
$.ui.log(`[npm-to-pnpm-rewriter] ${original} -> ${rewritten}`);
|
|
// Events are immutable: forward a modified copy instead of mutating e.
|
|
return next({ ...e, command: rewritten });
|
|
});
|
|
}
|