@hasna/hooks 0.12.2 → 0.12.4
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 +40 -7
- package/bin/hooks-mcp.js +2013 -847
- package/bin/index.js +2508 -1291
- package/bin/native-safety-entry.js +20 -15
- package/bin/native-safety-worker.cjs +20 -0
- package/dist/index.js +2268 -1047
- package/dist/lib/claude-skills-coordination.d.ts +25 -3
- package/dist/lib/codex-safety-check.d.ts +1 -1
- package/dist/lib/codex-safety-trust.d.ts +9 -2
- package/dist/lib/codex-settings.d.ts +6 -3
- package/dist/lib/installer.d.ts +9 -1
- package/dist/lib/native-safety-registration.d.ts +8 -2
- package/dist/native-safety.d.ts +4 -1
- package/dist/native-safety.js +2 -0
- package/hooks/codewith-native-common.ts +3 -3
- package/hooks/hook-signed-link-guard/src/hook.ts +5 -2
- package/hooks/hook-trash-guard/src/hook.ts +7 -4
- package/hooks/hook-workspace-repos-guard/README.md +20 -0
- package/hooks/hook-workspace-repos-guard/src/hook.ts +422 -10
- package/hooks/native-safety-composite.ts +75 -0
- package/hooks/native-safety-entry.ts +7 -53
- package/hooks/native-safety-worker.ts +19 -0
- package/package.json +5 -4
- package/scripts/build-native-worker.ts +152 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"main": "./dist/index.js",
|
|
34
34
|
"types": "./dist/index.d.ts",
|
|
35
35
|
"scripts": {
|
|
36
|
-
"build": "rm -rf dist bin && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external pg --external ink --external react --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/serve.ts --outdir ./bin --target bun --external pg && bun build ./src/mcp/hooks-mcp.ts --outdir ./bin --target bun --external pg --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/sdk/index.ts --outdir ./dist/sdk --target bun && bun build ./src/index.ts ./src/storage.ts --outdir ./dist --target bun --external pg && bun run build:native-safety && bun run build:types",
|
|
36
|
+
"build": "rm -rf dist bin && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external pg --external ink --external react --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/serve.ts --outdir ./bin --target bun --external pg && bun build ./src/mcp/hooks-mcp.ts --outdir ./bin --target bun --external pg --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/sdk/index.ts --outdir ./dist/sdk --target bun && bun build ./src/index.ts ./src/storage.ts --outdir ./dist --target bun --external pg && bun run build:native-safety && bun run build:native-safety-worker && bun run build:types",
|
|
37
37
|
"build:types": "tsc -p tsconfig.build.json",
|
|
38
38
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
39
39
|
"verify:generated": "bun run scripts/verify-generated-artifacts.ts",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"scan:artifact": "bun scripts/artifact-scan.ts",
|
|
47
47
|
"prepack": "bun run build && bun run scan:artifact",
|
|
48
48
|
"build:native-safety": "bun build ./hooks/native-safety-entry.ts --outdir ./bin --target bun --minify --define import.meta.main=false && bun build ./src/native-safety.ts --outdir ./dist --target bun && bun run build:native-readiness",
|
|
49
|
-
"build:native-readiness": "bun build ./hooks/native-readiness-entry.ts --outdir ./bin --target node --format=cjs && bun build ./src/native-readiness.ts --outdir ./dist --target bun"
|
|
49
|
+
"build:native-readiness": "bun build ./hooks/native-readiness-entry.ts --outdir ./bin --target node --format=cjs && bun build ./src/native-readiness.ts --outdir ./dist --target bun",
|
|
50
|
+
"build:native-safety-worker": "bun scripts/build-native-worker.ts"
|
|
50
51
|
},
|
|
51
52
|
"keywords": [
|
|
52
53
|
"claude-code",
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
"@hasna/contracts": "~1.2.1"
|
|
70
71
|
},
|
|
71
72
|
"dependencies": {
|
|
72
|
-
"@hasna/skills": "0.
|
|
73
|
+
"@hasna/skills": "0.10.28",
|
|
73
74
|
"@hasna/events": "^0.1.16",
|
|
74
75
|
"@hasna/secrets": "0.4.2",
|
|
75
76
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Build the native safety worker, ABI v2 (docs/adr/0001-stable-native-safety-launcher.md
|
|
4
|
+
* sections 3.3 and 3.4), into bin/native-safety-worker.cjs.
|
|
5
|
+
*
|
|
6
|
+
* The launcher's loader evaluates the verified bytes as a CommonJS function body,
|
|
7
|
+
* `new Function("module", "exports", "require", source)`, and passes a `require`
|
|
8
|
+
* that accepts only `node:` builtins. A bundle that needs anything else fails at
|
|
9
|
+
* every guarded call, so this build refuses it instead. Each check below fails
|
|
10
|
+
* the build, and nothing is written:
|
|
11
|
+
* - a `require(...)` whose specifier is not a literal `node:` builtin;
|
|
12
|
+
* - any `bun:` specifier, static import or export, dynamic `import()` or
|
|
13
|
+
* `import.meta`, none of which the loader's CommonJS shape can serve;
|
|
14
|
+
* - a top-level `await` (including `for await`);
|
|
15
|
+
* - a `require` used other than as a direct call, which would hide a specifier;
|
|
16
|
+
* - the version placeholder left unreplaced by the define;
|
|
17
|
+
* - a bundle that does not compile in the loader's function shape.
|
|
18
|
+
*
|
|
19
|
+
* Usage: bun scripts/build-native-worker.ts [--outfile <path>]
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { builtinModules } from "node:module";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import ts from "typescript";
|
|
26
|
+
|
|
27
|
+
export const WORKER_ENTRY = "hooks/native-safety-worker.ts";
|
|
28
|
+
export const WORKER_OUTFILE = "bin/native-safety-worker.cjs";
|
|
29
|
+
/** The identifier hooks/native-safety-worker.ts declares; the build replaces it with package.json's version. */
|
|
30
|
+
export const WORKER_VERSION_DEFINE = "HASNA_HOOKS_NATIVE_SAFETY_WORKER_VERSION";
|
|
31
|
+
|
|
32
|
+
const BUILTINS = new Set(builtinModules);
|
|
33
|
+
|
|
34
|
+
/** Whether `specifier` is exactly `node:<builtin>`, the only form the loader's require accepts. */
|
|
35
|
+
export function isNodeBuiltinSpecifier(specifier: string): boolean {
|
|
36
|
+
if (!specifier.startsWith("node:")) return false;
|
|
37
|
+
const name = specifier.slice("node:".length);
|
|
38
|
+
return /^[a-z][a-z0-9_]*(?:\/[a-z][a-z0-9_]*)*$/.test(name) && BUILTINS.has(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function literalText(node: ts.Node | undefined): string | undefined {
|
|
42
|
+
return node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether an identifier named `require` sits in a property-name position rather than referring to a binding. */
|
|
46
|
+
function isPropertyName(node: ts.Identifier): boolean {
|
|
47
|
+
const parent = node.parent;
|
|
48
|
+
return (ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
49
|
+
|| ((ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent) || ts.isPropertyDeclaration(parent)
|
|
50
|
+
|| ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent)) && parent.name === node);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every reason `source` cannot run under the v2 loader. Empty means the bundle
|
|
55
|
+
* passed. Parsed, not pattern-matched: a `require(` or `await` inside a string
|
|
56
|
+
* or a comment is data and is not reported.
|
|
57
|
+
*/
|
|
58
|
+
export function workerBundleProblems(source: string): string[] {
|
|
59
|
+
const problems: string[] = [];
|
|
60
|
+
const file = ts.createSourceFile("native-safety-worker.cjs", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
61
|
+
const at = (node: ts.Node) => {
|
|
62
|
+
const { line, character } = file.getLineAndCharacterOfPosition(node.getStart(file));
|
|
63
|
+
return `${line + 1}:${character + 1}`;
|
|
64
|
+
};
|
|
65
|
+
const specifier = (node: ts.Node, text: string, how: string) => {
|
|
66
|
+
if (text.startsWith("bun:")) problems.push(`bun: import ${JSON.stringify(text)} via ${how} at ${at(node)}`);
|
|
67
|
+
else if (how !== "require") problems.push(`${how} of ${JSON.stringify(text)}; the loader serves only require at ${at(node)}`);
|
|
68
|
+
else if (!isNodeBuiltinSpecifier(text)) problems.push(`require of non-node: specifier ${JSON.stringify(text)} at ${at(node)}`);
|
|
69
|
+
};
|
|
70
|
+
const visit = (node: ts.Node, inFunction: boolean): void => {
|
|
71
|
+
if (!inFunction && ts.isAwaitExpression(node)) problems.push(`top-level await at ${at(node)}`);
|
|
72
|
+
if (!inFunction && ts.isForOfStatement(node) && node.awaitModifier) problems.push(`top-level for await at ${at(node)}`);
|
|
73
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier) {
|
|
74
|
+
specifier(node, literalText(node.moduleSpecifier) ?? "<non-literal>", ts.isImportDeclaration(node) ? "static import" : "re-export");
|
|
75
|
+
} else if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isExportAssignment(node)
|
|
76
|
+
|| (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))) {
|
|
77
|
+
problems.push(`ESM syntax at ${at(node)}`);
|
|
78
|
+
}
|
|
79
|
+
if (ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword) problems.push(`import.meta at ${at(node)}`);
|
|
80
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
81
|
+
const text = node.arguments.length === 1 ? literalText(node.arguments[0]) : undefined;
|
|
82
|
+
specifier(node, text ?? "<non-literal>", "dynamic import()");
|
|
83
|
+
}
|
|
84
|
+
if (ts.isIdentifier(node) && node.text === WORKER_VERSION_DEFINE) problems.push(`version define not applied (${WORKER_VERSION_DEFINE}) at ${at(node)}`);
|
|
85
|
+
if (ts.isIdentifier(node) && node.text === "require" && !isPropertyName(node)) {
|
|
86
|
+
const call = node.parent;
|
|
87
|
+
if (ts.isCallExpression(call) && call.expression === node) {
|
|
88
|
+
const text = call.arguments.length === 1 ? literalText(call.arguments[0]) : undefined;
|
|
89
|
+
if (text === undefined) problems.push(`require with a non-literal specifier at ${at(call)}`);
|
|
90
|
+
else specifier(call, text, "require");
|
|
91
|
+
} else {
|
|
92
|
+
problems.push(`require used other than as a direct call at ${at(node)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const scope = inFunction || ts.isFunctionLike(node) || ts.isClassStaticBlockDeclaration(node);
|
|
96
|
+
ts.forEachChild(node, (child) => visit(child, scope));
|
|
97
|
+
};
|
|
98
|
+
visit(file, false);
|
|
99
|
+
try {
|
|
100
|
+
// Compile, never call: the loader's exact CommonJS function shape.
|
|
101
|
+
new Function("module", "exports", "require", source);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
problems.push(`does not compile as the loader's CommonJS function body: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
return problems;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface NativeSafetyWorkerBuild { outfile: string; bytes: number; sha256: string; version: string }
|
|
109
|
+
|
|
110
|
+
/** Bundle, check and write the worker. Writes nothing when a check fails. */
|
|
111
|
+
export async function buildNativeSafetyWorker(options: { packageDir?: string; outfile?: string } = {}): Promise<NativeSafetyWorkerBuild> {
|
|
112
|
+
const packageDir = resolve(options.packageDir ?? join(import.meta.dir, ".."));
|
|
113
|
+
const outfile = resolve(packageDir, options.outfile ?? WORKER_OUTFILE);
|
|
114
|
+
const version: unknown = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")).version;
|
|
115
|
+
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) throw new Error("package.json has no valid version for the native safety worker");
|
|
116
|
+
const result = await Bun.build({
|
|
117
|
+
entrypoints: [join(packageDir, WORKER_ENTRY)],
|
|
118
|
+
target: "node",
|
|
119
|
+
format: "cjs",
|
|
120
|
+
minify: true,
|
|
121
|
+
define: { "import.meta.main": "false", [WORKER_VERSION_DEFINE]: JSON.stringify(version) },
|
|
122
|
+
// Report the bundler's own messages (a parse error such as a top-level
|
|
123
|
+
// await in CommonJS) instead of a bare "Bundle failed".
|
|
124
|
+
throw: false,
|
|
125
|
+
});
|
|
126
|
+
if (!result.success) throw new Error(`native safety worker build failed:\n${result.logs.map(String).join("\n")}`);
|
|
127
|
+
if (result.outputs.length !== 1) throw new Error(`native safety worker build produced ${result.outputs.length} outputs, expected 1`);
|
|
128
|
+
const source = await result.outputs[0]!.text();
|
|
129
|
+
const problems = workerBundleProblems(source);
|
|
130
|
+
if (problems.length > 0) throw new Error(`native safety worker bundle refused:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
131
|
+
mkdirSync(dirname(outfile), { recursive: true });
|
|
132
|
+
// A new file is never group- or world-writable, whatever the umask: the launcher's pin check refuses such a worker.
|
|
133
|
+
writeFileSync(outfile, source, { mode: 0o644 });
|
|
134
|
+
return { outfile, bytes: Buffer.byteLength(source), sha256: createHash("sha256").update(source).digest("hex"), version };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (import.meta.main) {
|
|
138
|
+
const args = process.argv.slice(2);
|
|
139
|
+
let outfile: string | undefined;
|
|
140
|
+
if (args.length === 2 && args[0] === "--outfile" && args[1]) outfile = resolve(args[1]);
|
|
141
|
+
else if (args.length !== 0) {
|
|
142
|
+
console.error("usage: bun scripts/build-native-worker.ts [--outfile <path>]");
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const built = await buildNativeSafetyWorker({ outfile });
|
|
147
|
+
console.log(`native-safety-worker.cjs ${built.version}: ${built.bytes} bytes, sha256 ${built.sha256} -> ${built.outfile}`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|