@elnop/agent-hook 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # Temporary Holding Version
1
+ # @elnop/agent-hook
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Claude Code `PostToolUse` hook. After each `Edit` or `Write` by the agent:
4
+
5
+ - an edited `.ts` or `.tsx` file goes through `oxlint --type-aware`, run from the folder of the nearest `oxlint.config.ts` or `.oxlintrc.json`, so a monorepo with a config per workspace works as is
6
+ - a created file, or an edited Markdown file, runs `alint check` if the project has an `.alint.yml`
7
+ - a created file inside one of the `--tree` folders runs `check-tree` on that folder (default: `src`)
8
+
9
+ If one of them fails, the hook prints their output and exits with code 2: Claude Code sends it back to the agent, which fixes the problem before going on. The tools are run with `npx --no-install`, so they must be dependencies of the project.
10
+
11
+ ```sh
12
+ npm install --save-dev @elnop/agent-hook
13
+ ```
14
+
15
+ `.claude/settings.json`:
16
+
17
+ ```json
18
+ {
19
+ "hooks": {
20
+ "PostToolUse": [
21
+ {
22
+ "matcher": "Edit|Write",
23
+ "hooks": [{ "type": "command", "command": "npx --no-install elnop-agent-hook --tree src", "timeout": 60 }]
24
+ }
25
+ ]
26
+ }
27
+ }
28
+ ```
29
+
30
+ Pass `--tree` once per source folder (`--tree web/src --tree server/src`). The project root is `$CLAUDE_PROJECT_DIR`, or `--root`.
31
+
32
+ Without touching `.claude/settings.json`, the `elnop-checks` plugin from this repository runs the same hook in any project that has the package installed. See the [repository README](../../../README.md#claude-code-plugin).
@@ -0,0 +1,10 @@
1
+ export type HookCheck = {
2
+ label: string;
3
+ command: string;
4
+ args: string[];
5
+ cwd: string;
6
+ };
7
+ export type HookEdit = {
8
+ file: string;
9
+ created: boolean;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { parseArgs } from 'node:util';
6
+ import { hookChecks } from './hookChecks.js';
7
+ import { readHookEdit } from './readHookEdit.js';
8
+ const { values } = parseArgs({ options: { tree: { type: 'string', multiple: true, default: ['src'] }, root: { type: 'string' } } });
9
+ const root = resolve(values.root ?? process.env['CLAUDE_PROJECT_DIR'] ?? process.cwd());
10
+ const edit = readHookEdit(JSON.parse(readFileSync(0, 'utf8')));
11
+ const problems = [];
12
+ const file = edit.file === '' ? '' : resolve(root, edit.file);
13
+ for (const check of hookChecks(root, { ...edit, file }, values.tree)) {
14
+ const result = spawnSync('npx', ['--no-install', check.command, ...check.args], { cwd: check.cwd, encoding: 'utf8' });
15
+ if (result.status !== 0)
16
+ problems.push(`${check.label}:\n${result.stdout}${result.stderr}`);
17
+ }
18
+ if (problems.length > 0) {
19
+ process.stderr.write(`${problems.join('\n')}\nFix them before going on.\n`);
20
+ process.exitCode = 2;
21
+ }
@@ -0,0 +1,2 @@
1
+ import type { HookCheck, HookEdit } from './AgentHookTypes.ts';
2
+ export declare function hookChecks(root: string, { file, created }: HookEdit, trees: string[]): HookCheck[];
@@ -0,0 +1,35 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, relative } from 'node:path';
3
+ const OXLINT_CONFIGS = ['oxlint.config.ts', '.oxlintrc.json'];
4
+ function inside(root, path) {
5
+ const fromRoot = relative(root, path);
6
+ return !fromRoot.startsWith('..') && !fromRoot.split('/').includes('node_modules');
7
+ }
8
+ function oxlintFolder(root, file) {
9
+ for (let folder = dirname(file); inside(root, folder); folder = dirname(folder)) {
10
+ if (OXLINT_CONFIGS.some((config) => existsSync(join(folder, config))))
11
+ return folder;
12
+ if (folder === root)
13
+ break;
14
+ }
15
+ return root;
16
+ }
17
+ export function hookChecks(root, { file, created }, trees) {
18
+ if (file === '' || !inside(root, file))
19
+ return [];
20
+ const fromRoot = relative(root, file);
21
+ const checks = [];
22
+ if (/\.tsx?$/u.test(file)) {
23
+ const cwd = oxlintFolder(root, file);
24
+ checks.push({ label: `oxlint found problems in ${fromRoot}`, command: 'oxlint', args: ['--type-aware', relative(cwd, file)], cwd });
25
+ }
26
+ if ((created || file.endsWith('.md')) && existsSync(join(root, '.alint.yml'))) {
27
+ checks.push({ label: 'alint found problems in the repository layout or the docs', command: 'alint', args: ['check'], cwd: root });
28
+ }
29
+ if (created) {
30
+ for (const tree of trees.filter((folder) => fromRoot.startsWith(`${folder}/`))) {
31
+ checks.push({ label: `check-tree found problems in ${tree}`, command: 'check-tree', args: [tree], cwd: root });
32
+ }
33
+ }
34
+ return checks;
35
+ }
@@ -0,0 +1,2 @@
1
+ import type { HookEdit } from './AgentHookTypes.ts';
2
+ export declare function readHookEdit(input: unknown): HookEdit;
@@ -0,0 +1,7 @@
1
+ function field(value, key) {
2
+ return typeof value === 'object' && value !== null ? Reflect.get(value, key) : undefined;
3
+ }
4
+ export function readHookEdit(input) {
5
+ const filePath = field(field(input, 'tool_input'), 'file_path');
6
+ return { file: typeof filePath === 'string' ? filePath : '', created: field(input, 'tool_name') === 'Write' };
7
+ }
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@elnop/agent-hook",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Claude Code PostToolUse hook: lints each file the agent edits with oxlint, runs alint and check-tree when a file is created, and sends the errors back to the agent.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "hook",
8
+ "oxlint",
9
+ "alint",
10
+ "agent"
11
+ ],
12
+ "homepage": "https://github.com/Elnop/elnop-tooling/tree/main/packages/tools/agent-hook#readme",
13
+ "bugs": "https://github.com/Elnop/elnop-tooling/issues",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Elnop/elnop-tooling.git",
17
+ "directory": "packages/tools/agent-hook"
18
+ },
19
+ "license": "MIT",
20
+ "author": "elnop",
21
+ "type": "module",
22
+ "files": [
23
+ "dist",
24
+ "!dist/tsconfig.tsbuildinfo"
25
+ ],
26
+ "bin": {
27
+ "elnop-agent-hook": "./dist/agentHook.js"
28
+ },
29
+ "scripts": {
30
+ "test": "node --test '**/*.test.ts'"
31
+ }
32
+ }