@hasna/hooks 0.9.5 → 0.9.6
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 +27 -0
- package/bin/hooks-mcp.js +282 -123
- package/bin/index.js +625 -252
- package/bin/native-safety-entry.js +1136 -0
- package/bin/serve.js +329 -127
- package/dist/index.js +610 -249
- package/dist/lib/native-safety.d.ts +15 -0
- package/dist/openapi.d.ts +314 -45
- package/dist/sdk/authority.d.ts +31 -0
- package/dist/sdk/generated.d.ts +97 -0
- package/dist/sdk/index.d.ts +14 -15
- package/dist/sdk/index.js +172 -1
- package/dist/sdk/registry-client.d.ts +75 -0
- package/hooks/native-safety-entry.ts +28 -0
- package/package.json +7 -4
- package/scripts/generate-sdk.ts +60 -0
- package/scripts/validate-package.ts +34 -0
- package/scripts/verify-generated-artifacts.ts +237 -0
package/bin/index.js
CHANGED
|
@@ -2075,6 +2075,7 @@ var init_registry = __esm(() => {
|
|
|
2075
2075
|
},
|
|
2076
2076
|
{
|
|
2077
2077
|
name: "workspace-repos-guard",
|
|
2078
|
+
timeoutSeconds: 10,
|
|
2078
2079
|
displayName: "Workspace Repos Guard",
|
|
2079
2080
|
description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
|
|
2080
2081
|
version: "0.1.0",
|
|
@@ -2093,7 +2094,7 @@ var init_registry = __esm(() => {
|
|
|
2093
2094
|
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
2094
2095
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
2095
2096
|
rewritesInput: true,
|
|
2096
|
-
timeoutSeconds:
|
|
2097
|
+
timeoutSeconds: 10
|
|
2097
2098
|
},
|
|
2098
2099
|
{
|
|
2099
2100
|
name: "checktests",
|
|
@@ -4500,6 +4501,139 @@ var init_resolve = __esm(() => {
|
|
|
4500
4501
|
init_manifest();
|
|
4501
4502
|
});
|
|
4502
4503
|
|
|
4504
|
+
// src/lib/native-safety.ts
|
|
4505
|
+
import { createHash } from "crypto";
|
|
4506
|
+
import { closeSync, constants, existsSync as existsSync7, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
|
|
4507
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join8 } from "path";
|
|
4508
|
+
import { homedir as homedir3 } from "os";
|
|
4509
|
+
function isNativeSafetyName(name) {
|
|
4510
|
+
return name === "trash-guard" || name === "workspace-repos-guard";
|
|
4511
|
+
}
|
|
4512
|
+
function validateBinding(b) {
|
|
4513
|
+
if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
|
|
4514
|
+
throw new Error("Invalid native safety binding");
|
|
4515
|
+
}
|
|
4516
|
+
function buildNativeSafetyCommand(binding) {
|
|
4517
|
+
validateBinding(binding);
|
|
4518
|
+
const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
|
|
4519
|
+
return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
|
|
4520
|
+
}
|
|
4521
|
+
function nativeSafetyRegistration(command) {
|
|
4522
|
+
if (!command.startsWith(prefix))
|
|
4523
|
+
return;
|
|
4524
|
+
const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
|
|
4525
|
+
if (!encoded || encoded.length > 16384)
|
|
4526
|
+
return;
|
|
4527
|
+
try {
|
|
4528
|
+
const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
4529
|
+
return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
|
|
4530
|
+
} catch {
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
function pinEligibleFile(file, maximum, executable = false) {
|
|
4535
|
+
if (!isAbsolute2(file) || realpathSync2(file) !== file)
|
|
4536
|
+
throw new Error("Native safety requires canonical files");
|
|
4537
|
+
for (let parent = dirname3(file);; ) {
|
|
4538
|
+
const s = lstatSync(parent);
|
|
4539
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
|
|
4540
|
+
throw new Error("Native safety requires trusted, non-writable file ancestry");
|
|
4541
|
+
if (parent === dirname3(parent))
|
|
4542
|
+
break;
|
|
4543
|
+
parent = dirname3(parent);
|
|
4544
|
+
}
|
|
4545
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
4546
|
+
try {
|
|
4547
|
+
const s = fstatSync(fd);
|
|
4548
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
|
|
4549
|
+
throw new Error("Native safety requires an eligible installed file");
|
|
4550
|
+
const bytes = readFileSync2(fd);
|
|
4551
|
+
if (bytes.length > maximum)
|
|
4552
|
+
throw new Error("Native safety file exceeds its bound");
|
|
4553
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
4554
|
+
} finally {
|
|
4555
|
+
closeSync(fd);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
function installedNativeSafetyCommand(name) {
|
|
4559
|
+
const home = realpathSync2(homedir3());
|
|
4560
|
+
const runtime = realpathSync2(process.execPath);
|
|
4561
|
+
const candidates = [join8(import.meta.dir, "../../bin/native-safety-entry.js"), join8(import.meta.dir, "../bin/native-safety-entry.js"), join8(import.meta.dir, "native-safety-entry.js")];
|
|
4562
|
+
const worker = candidates.find(existsSync7);
|
|
4563
|
+
if (!worker)
|
|
4564
|
+
throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
|
|
4565
|
+
const canonicalWorker = realpathSync2(worker);
|
|
4566
|
+
return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join8(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
|
|
4567
|
+
}
|
|
4568
|
+
var supervisorSource, quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'", program2, shell, prefix;
|
|
4569
|
+
var init_native_safety = __esm(() => {
|
|
4570
|
+
supervisorSource = String.raw`async function supervise(encoded) {
|
|
4571
|
+
const fs = require("node:fs");
|
|
4572
|
+
const path = require("node:path");
|
|
4573
|
+
const crypto = require("node:crypto");
|
|
4574
|
+
const cp = require("node:child_process");
|
|
4575
|
+
const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
|
|
4576
|
+
try {
|
|
4577
|
+
const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
4578
|
+
const check = (file, hash, maximum) => {
|
|
4579
|
+
if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
|
|
4580
|
+
for (let parent = path.dirname(file);;) {
|
|
4581
|
+
const s = fs.lstatSync(parent);
|
|
4582
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
|
|
4583
|
+
if (parent === path.dirname(parent)) break;
|
|
4584
|
+
parent = path.dirname(parent);
|
|
4585
|
+
}
|
|
4586
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
4587
|
+
try {
|
|
4588
|
+
const s = fs.fstatSync(fd);
|
|
4589
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
|
|
4590
|
+
const bytes = fs.readFileSync(fd);
|
|
4591
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
|
|
4592
|
+
return bytes;
|
|
4593
|
+
} finally { fs.closeSync(fd); }
|
|
4594
|
+
};
|
|
4595
|
+
check(b.runtime, b.runtimeSHA256, 200_000_000);
|
|
4596
|
+
const bytes = check(b.worker, b.workerSHA256, 256_000);
|
|
4597
|
+
const deadline = setTimeout(fail, 1500);
|
|
4598
|
+
const chunks = []; let size = 0;
|
|
4599
|
+
for await (const chunk of process.stdin) {
|
|
4600
|
+
size += chunk.length;
|
|
4601
|
+
if (size > 1_048_576) throw Error();
|
|
4602
|
+
chunks.push(chunk);
|
|
4603
|
+
}
|
|
4604
|
+
clearTimeout(deadline);
|
|
4605
|
+
const input = Buffer.concat(chunks);
|
|
4606
|
+
const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
|
|
4607
|
+
input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
|
|
4608
|
+
});
|
|
4609
|
+
if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
|
|
4610
|
+
const result = JSON.parse(child.stdout.toString("utf8"));
|
|
4611
|
+
if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
|
|
4612
|
+
if (result.verdict === null) return;
|
|
4613
|
+
const output = result.verdict?.hookSpecificOutput;
|
|
4614
|
+
if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
|
|
4615
|
+
if (output.permissionDecision === "deny") {
|
|
4616
|
+
if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
|
|
4617
|
+
} else if (output.permissionDecision === "allow") {
|
|
4618
|
+
if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
|
|
4619
|
+
} else throw Error();
|
|
4620
|
+
process.stdout.write(JSON.stringify(result.verdict));
|
|
4621
|
+
} catch { fail(); }
|
|
4622
|
+
}`;
|
|
4623
|
+
program2 = `(${supervisorSource})(process.argv[1]);`;
|
|
4624
|
+
shell = `cd / || exit 2
|
|
4625
|
+
exec 3<&0 || exit 2
|
|
4626
|
+
/usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program2)} "$1" <&3 3<&- 2>/dev/null &
|
|
4627
|
+
runner=$!
|
|
4628
|
+
exec 3<&-
|
|
4629
|
+
(trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
|
|
4630
|
+
watchdog=$!
|
|
4631
|
+
wait "$runner"; result=$?
|
|
4632
|
+
kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
|
|
4633
|
+
if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
|
|
4634
|
+
prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
|
|
4635
|
+
});
|
|
4636
|
+
|
|
4503
4637
|
// src/lib/mementos-options.ts
|
|
4504
4638
|
function validateMementosRegistration(name, value) {
|
|
4505
4639
|
if (value === undefined)
|
|
@@ -4570,6 +4704,9 @@ function mementosRunEnvironment(env, value) {
|
|
|
4570
4704
|
function registrationName(command) {
|
|
4571
4705
|
if (typeof command !== "string")
|
|
4572
4706
|
return;
|
|
4707
|
+
const native = nativeSafetyRegistration(command);
|
|
4708
|
+
if (native)
|
|
4709
|
+
return native;
|
|
4573
4710
|
if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
|
|
4574
4711
|
try {
|
|
4575
4712
|
retainedMementosRegistration([command.trim()]);
|
|
@@ -4662,8 +4799,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
4662
4799
|
if (!Array.isArray(hooks))
|
|
4663
4800
|
return false;
|
|
4664
4801
|
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
4665
|
-
|
|
4666
|
-
if (!match || match[1] !== name)
|
|
4802
|
+
if (registrationName(h?.command) !== name)
|
|
4667
4803
|
return false;
|
|
4668
4804
|
if (!matcher)
|
|
4669
4805
|
return true;
|
|
@@ -4675,6 +4811,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
4675
4811
|
}
|
|
4676
4812
|
var REGISTRATION_COMMAND_RE;
|
|
4677
4813
|
var init_registration = __esm(() => {
|
|
4814
|
+
init_native_safety();
|
|
4678
4815
|
init_resolve();
|
|
4679
4816
|
REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|hook-([\w-]+))$/;
|
|
4680
4817
|
});
|
|
@@ -4873,9 +5010,9 @@ var init_migrations = __esm(() => {
|
|
|
4873
5010
|
});
|
|
4874
5011
|
|
|
4875
5012
|
// src/db/legacy-import.ts
|
|
4876
|
-
import { existsSync as
|
|
4877
|
-
import { join as
|
|
4878
|
-
import { homedir as
|
|
5013
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
5014
|
+
import { join as join9 } from "path";
|
|
5015
|
+
import { homedir as homedir4 } from "os";
|
|
4879
5016
|
function ensureMetaTable(db) {
|
|
4880
5017
|
db.exec(`
|
|
4881
5018
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -4898,7 +5035,7 @@ function nanoid() {
|
|
|
4898
5035
|
function importJsonlFile(db, filePath) {
|
|
4899
5036
|
let count = 0;
|
|
4900
5037
|
try {
|
|
4901
|
-
const lines =
|
|
5038
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4902
5039
|
`).filter(Boolean);
|
|
4903
5040
|
for (const line of lines) {
|
|
4904
5041
|
try {
|
|
@@ -4924,7 +5061,7 @@ function importJsonlFile(db, filePath) {
|
|
|
4924
5061
|
function importErrorsLog(db, filePath) {
|
|
4925
5062
|
let count = 0;
|
|
4926
5063
|
try {
|
|
4927
|
-
const lines =
|
|
5064
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4928
5065
|
`).filter(Boolean);
|
|
4929
5066
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4930
5067
|
for (const line of lines) {
|
|
@@ -4949,25 +5086,25 @@ function importErrorsLog(db, filePath) {
|
|
|
4949
5086
|
} catch {}
|
|
4950
5087
|
return count;
|
|
4951
5088
|
}
|
|
4952
|
-
function runLegacyImport(db, homeDir =
|
|
5089
|
+
function runLegacyImport(db, homeDir = homedir4()) {
|
|
4953
5090
|
try {
|
|
4954
5091
|
if (isAlreadyDone(db))
|
|
4955
5092
|
return;
|
|
4956
5093
|
let total = 0;
|
|
4957
|
-
const claudeProjectsDir =
|
|
4958
|
-
if (
|
|
5094
|
+
const claudeProjectsDir = join9(homeDir, ".claude", "projects");
|
|
5095
|
+
if (existsSync8(claudeProjectsDir)) {
|
|
4959
5096
|
try {
|
|
4960
5097
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
4961
5098
|
for (const dir of projectDirs) {
|
|
4962
|
-
const projectDir =
|
|
5099
|
+
const projectDir = join9(claudeProjectsDir, dir);
|
|
4963
5100
|
try {
|
|
4964
5101
|
const files = readdirSync2(projectDir);
|
|
4965
5102
|
for (const file of files) {
|
|
4966
5103
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4967
|
-
total += importJsonlFile(db,
|
|
5104
|
+
total += importJsonlFile(db, join9(projectDir, file));
|
|
4968
5105
|
}
|
|
4969
5106
|
if (file === "errors.log") {
|
|
4970
|
-
total += importErrorsLog(db,
|
|
5107
|
+
total += importErrorsLog(db, join9(projectDir, file));
|
|
4971
5108
|
}
|
|
4972
5109
|
}
|
|
4973
5110
|
} catch {}
|
|
@@ -5014,8 +5151,8 @@ __export(exports_db, {
|
|
|
5014
5151
|
allowLocalStore: () => allowLocalStore
|
|
5015
5152
|
});
|
|
5016
5153
|
import { Database } from "bun:sqlite";
|
|
5017
|
-
import { existsSync as
|
|
5018
|
-
import { join as
|
|
5154
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
5155
|
+
import { join as join10 } from "path";
|
|
5019
5156
|
function refuseLocalStore(message) {
|
|
5020
5157
|
localStoreRefusal = message;
|
|
5021
5158
|
}
|
|
@@ -5030,8 +5167,8 @@ function localStoreRefusalMessage() {
|
|
|
5030
5167
|
}
|
|
5031
5168
|
function resolveDataDir() {
|
|
5032
5169
|
const effective = getEffectiveDataRoot();
|
|
5033
|
-
const oldDir =
|
|
5034
|
-
if (!
|
|
5170
|
+
const oldDir = join10(getHomeDir(), ".hooks");
|
|
5171
|
+
if (!existsSync9(effective) && existsSync9(oldDir)) {
|
|
5035
5172
|
mkdirSync2(effective, { recursive: true });
|
|
5036
5173
|
cpSync(oldDir, effective, { recursive: true });
|
|
5037
5174
|
}
|
|
@@ -5042,11 +5179,11 @@ function getDbPath() {
|
|
|
5042
5179
|
if (explicitDb)
|
|
5043
5180
|
return explicitDb;
|
|
5044
5181
|
const dataDir3 = resolveDataDir();
|
|
5045
|
-
return
|
|
5182
|
+
return join10(dataDir3, "hooks.db");
|
|
5046
5183
|
}
|
|
5047
5184
|
function ensureDir(dbPath) {
|
|
5048
5185
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
5049
|
-
if (dir && !
|
|
5186
|
+
if (dir && !existsSync9(dir)) {
|
|
5050
5187
|
mkdirSync2(dir, { recursive: true });
|
|
5051
5188
|
}
|
|
5052
5189
|
}
|
|
@@ -5056,7 +5193,7 @@ function getDb() {
|
|
|
5056
5193
|
if (instance)
|
|
5057
5194
|
return instance;
|
|
5058
5195
|
const dbPath = getDbPath();
|
|
5059
|
-
const isNew = dbPath === ":memory:" || !
|
|
5196
|
+
const isNew = dbPath === ":memory:" || !existsSync9(dbPath);
|
|
5060
5197
|
ensureDir(dbPath);
|
|
5061
5198
|
instance = new Database(dbPath);
|
|
5062
5199
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -5118,12 +5255,12 @@ __export(exports_store, {
|
|
|
5118
5255
|
checkScriptHash: () => checkScriptHash,
|
|
5119
5256
|
LockFileError: () => LockFileError
|
|
5120
5257
|
});
|
|
5121
|
-
import { createHash } from "crypto";
|
|
5122
|
-
import { existsSync as
|
|
5258
|
+
import { createHash as createHash2 } from "crypto";
|
|
5259
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
5123
5260
|
import { readFile as readFile2 } from "fs/promises";
|
|
5124
5261
|
import { randomBytes } from "crypto";
|
|
5125
5262
|
function sha256Of(input) {
|
|
5126
|
-
return
|
|
5263
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
5127
5264
|
}
|
|
5128
5265
|
async function sha256File(path) {
|
|
5129
5266
|
const buf = await readFile2(path);
|
|
@@ -5184,11 +5321,11 @@ function assertLockShape(parsed, path) {
|
|
|
5184
5321
|
}
|
|
5185
5322
|
function readLock() {
|
|
5186
5323
|
const path = getLockPath();
|
|
5187
|
-
if (!
|
|
5324
|
+
if (!existsSync10(path))
|
|
5188
5325
|
return { hooks: {} };
|
|
5189
5326
|
let raw;
|
|
5190
5327
|
try {
|
|
5191
|
-
raw =
|
|
5328
|
+
raw = readFileSync4(path, "utf-8");
|
|
5192
5329
|
} catch {
|
|
5193
5330
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5194
5331
|
}
|
|
@@ -5291,7 +5428,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5291
5428
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5292
5429
|
}
|
|
5293
5430
|
function retrustHook(name, scriptPath, version, source) {
|
|
5294
|
-
const actual = sha256Of(
|
|
5431
|
+
const actual = sha256Of(readFileSync4(scriptPath));
|
|
5295
5432
|
const db = storeDb();
|
|
5296
5433
|
if (db) {
|
|
5297
5434
|
upsertHookRecord(db, {
|
|
@@ -5318,15 +5455,15 @@ var init_store = __esm(() => {
|
|
|
5318
5455
|
});
|
|
5319
5456
|
|
|
5320
5457
|
// src/lib/codex-settings.ts
|
|
5321
|
-
import { closeSync, existsSync as
|
|
5322
|
-
import { dirname as
|
|
5458
|
+
import { closeSync as closeSync2, existsSync as existsSync11, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5459
|
+
import { dirname as dirname4 } from "path";
|
|
5323
5460
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5324
5461
|
function current(path) {
|
|
5325
5462
|
try {
|
|
5326
|
-
const stat =
|
|
5463
|
+
const stat = lstatSync2(path);
|
|
5327
5464
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
5328
5465
|
throw new Error("invalid_file");
|
|
5329
|
-
return
|
|
5466
|
+
return readFileSync5(path, "utf8");
|
|
5330
5467
|
} catch (error) {
|
|
5331
5468
|
if (error.code === "ENOENT")
|
|
5332
5469
|
return null;
|
|
@@ -5358,11 +5495,11 @@ function writeCodexSettings(path, settings) {
|
|
|
5358
5495
|
const snapshot = snapshots.get(settings);
|
|
5359
5496
|
if (!snapshot || snapshot.path !== path)
|
|
5360
5497
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
5361
|
-
mkdirSync4(
|
|
5498
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
5362
5499
|
const lock = `${path}.hooks-write.lock`;
|
|
5363
5500
|
let fd;
|
|
5364
5501
|
try {
|
|
5365
|
-
fd =
|
|
5502
|
+
fd = openSync2(lock, "wx", 384);
|
|
5366
5503
|
} catch {
|
|
5367
5504
|
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
5368
5505
|
}
|
|
@@ -5378,9 +5515,9 @@ function writeCodexSettings(path, settings) {
|
|
|
5378
5515
|
renameSync2(temp, path);
|
|
5379
5516
|
snapshots.set(settings, { path, raw: next });
|
|
5380
5517
|
} finally {
|
|
5381
|
-
if (
|
|
5518
|
+
if (existsSync11(temp))
|
|
5382
5519
|
unlinkSync(temp);
|
|
5383
|
-
|
|
5520
|
+
closeSync2(fd);
|
|
5384
5521
|
unlinkSync(lock);
|
|
5385
5522
|
}
|
|
5386
5523
|
}
|
|
@@ -5409,9 +5546,9 @@ __export(exports_installer, {
|
|
|
5409
5546
|
detectRewriteConflict: () => detectRewriteConflict,
|
|
5410
5547
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5411
5548
|
});
|
|
5412
|
-
import { existsSync as
|
|
5413
|
-
import { join as
|
|
5414
|
-
import { homedir as
|
|
5549
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5550
|
+
import { join as join11, dirname as dirname5 } from "path";
|
|
5551
|
+
import { homedir as homedir5 } from "os";
|
|
5415
5552
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5416
5553
|
function normalizeHookName(name) {
|
|
5417
5554
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
@@ -5424,8 +5561,7 @@ function removeHookEntriesByName(entries, hookName) {
|
|
|
5424
5561
|
if (!Array.isArray(entry.hooks))
|
|
5425
5562
|
return [entry];
|
|
5426
5563
|
const kept = entry.hooks.filter((h) => {
|
|
5427
|
-
|
|
5428
|
-
return !(match && match[1] === hookName);
|
|
5564
|
+
return registrationName(h.command) !== hookName;
|
|
5429
5565
|
});
|
|
5430
5566
|
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
5431
5567
|
});
|
|
@@ -5459,29 +5595,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
5459
5595
|
return globalOverride;
|
|
5460
5596
|
const dir = getTargetSettingsDir(target);
|
|
5461
5597
|
if (target === "codex")
|
|
5462
|
-
return
|
|
5598
|
+
return join11(scope === "project" ? process.cwd() : homedir5(), dir, "hooks.json");
|
|
5463
5599
|
if (scope === "project") {
|
|
5464
|
-
return target === "codewith" ?
|
|
5600
|
+
return target === "codewith" ? join11(process.cwd(), dir, "config.toml") : join11(process.cwd(), dir, "settings.json");
|
|
5465
5601
|
}
|
|
5466
|
-
return target === "codewith" ?
|
|
5602
|
+
return target === "codewith" ? join11(homedir5(), dir, "config.toml") : join11(homedir5(), dir, "settings.json");
|
|
5467
5603
|
}
|
|
5468
5604
|
function getHookPath(name) {
|
|
5469
5605
|
const shortName = shortHookName(name);
|
|
5470
5606
|
const resolved = resolveHookDir(shortName);
|
|
5471
5607
|
if (resolved)
|
|
5472
5608
|
return resolved;
|
|
5473
|
-
return
|
|
5609
|
+
return join11(HOOKS_DIR, normalizeHookName(shortName));
|
|
5474
5610
|
}
|
|
5475
5611
|
function hookExists(name) {
|
|
5476
|
-
return
|
|
5612
|
+
return existsSync12(getHookPath(name));
|
|
5477
5613
|
}
|
|
5478
5614
|
function readSettings(scope = "global", target = "claude") {
|
|
5479
5615
|
const path = getSettingsPath(scope, target);
|
|
5480
5616
|
if (target === "codex")
|
|
5481
5617
|
return readCodexSettings(path);
|
|
5482
5618
|
try {
|
|
5483
|
-
if (
|
|
5484
|
-
return JSON.parse(
|
|
5619
|
+
if (existsSync12(path)) {
|
|
5620
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
5485
5621
|
}
|
|
5486
5622
|
} catch (error) {
|
|
5487
5623
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5492,8 +5628,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
5492
5628
|
const path = getSettingsPath(scope, target);
|
|
5493
5629
|
if (target === "codex")
|
|
5494
5630
|
return writeCodexSettings(path, settings);
|
|
5495
|
-
const dir =
|
|
5496
|
-
if (!
|
|
5631
|
+
const dir = dirname5(path);
|
|
5632
|
+
if (!existsSync12(dir)) {
|
|
5497
5633
|
mkdirSync5(dir, { recursive: true });
|
|
5498
5634
|
}
|
|
5499
5635
|
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -5521,6 +5657,9 @@ function codewithTimeout(name) {
|
|
|
5521
5657
|
switch (name) {
|
|
5522
5658
|
case "mementos-context":
|
|
5523
5659
|
return 12;
|
|
5660
|
+
case "trash-guard":
|
|
5661
|
+
case "workspace-repos-guard":
|
|
5662
|
+
return 10;
|
|
5524
5663
|
case "knowledge-context":
|
|
5525
5664
|
return 6;
|
|
5526
5665
|
case "session-start":
|
|
@@ -5530,7 +5669,6 @@ function codewithTimeout(name) {
|
|
|
5530
5669
|
case "prompt-guard":
|
|
5531
5670
|
return 3;
|
|
5532
5671
|
case "worktree-guard":
|
|
5533
|
-
case "trash-guard":
|
|
5534
5672
|
case "stop-sync":
|
|
5535
5673
|
return 5;
|
|
5536
5674
|
default:
|
|
@@ -5559,11 +5697,13 @@ function codewithStatusMessage(name) {
|
|
|
5559
5697
|
}
|
|
5560
5698
|
function buildCodewithTomlFragment(name, profile, mementos) {
|
|
5561
5699
|
const shortName = shortHookName(name);
|
|
5562
|
-
|
|
5700
|
+
if (isNativeSafetyName(shortName) && profile)
|
|
5701
|
+
throw new Error("Bundled native safety guards do not select registry profiles");
|
|
5702
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
5563
5703
|
if (!meta)
|
|
5564
5704
|
throw new Error(`Hook '${shortName}' not found`);
|
|
5565
5705
|
validateMementosRegistration(shortName, mementos);
|
|
5566
|
-
const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
5706
|
+
const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
5567
5707
|
const matcher = codewithMatcher(meta.matcher);
|
|
5568
5708
|
const fragments = [];
|
|
5569
5709
|
for (const event of getHookEvents(meta)) {
|
|
@@ -5588,17 +5728,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
|
|
|
5588
5728
|
function readCodewithConfig(scope, configPath) {
|
|
5589
5729
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5590
5730
|
try {
|
|
5591
|
-
return
|
|
5731
|
+
return existsSync12(path) ? readFileSync6(path, "utf-8") : "";
|
|
5592
5732
|
} catch {
|
|
5593
5733
|
return "";
|
|
5594
5734
|
}
|
|
5595
5735
|
}
|
|
5596
5736
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
5597
5737
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5598
|
-
const dir =
|
|
5599
|
-
if (!
|
|
5738
|
+
const dir = dirname5(path);
|
|
5739
|
+
if (!existsSync12(dir))
|
|
5600
5740
|
mkdirSync5(dir, { recursive: true });
|
|
5601
|
-
const existing =
|
|
5741
|
+
const existing = existsSync12(path) ? readFileSync6(path, "utf-8") : "";
|
|
5602
5742
|
const sep2 = existing.trim() ? `
|
|
5603
5743
|
|
|
5604
5744
|
` : "";
|
|
@@ -5606,7 +5746,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
5606
5746
|
return path;
|
|
5607
5747
|
}
|
|
5608
5748
|
function detectConflict(name, scope, target) {
|
|
5609
|
-
const meta = resolveHookMeta(name);
|
|
5749
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
5610
5750
|
if (!meta || !meta.matcher)
|
|
5611
5751
|
return;
|
|
5612
5752
|
const events = new Set(getHookEvents(meta));
|
|
@@ -5626,7 +5766,7 @@ function detectConflict(name, scope, target) {
|
|
|
5626
5766
|
return;
|
|
5627
5767
|
}
|
|
5628
5768
|
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
5629
|
-
const meta = resolveHookMeta(name);
|
|
5769
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
5630
5770
|
if (!meta?.matcher || !meta.rewritesInput)
|
|
5631
5771
|
return;
|
|
5632
5772
|
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
@@ -5647,7 +5787,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
5647
5787
|
}
|
|
5648
5788
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
5649
5789
|
const shortName = shortHookName(name);
|
|
5650
|
-
if (
|
|
5790
|
+
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
5791
|
+
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
5792
|
+
}
|
|
5793
|
+
if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
|
|
5651
5794
|
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
5652
5795
|
}
|
|
5653
5796
|
if (target === "codewith") {
|
|
@@ -5739,7 +5882,7 @@ function installHook(name, options = {}) {
|
|
|
5739
5882
|
}
|
|
5740
5883
|
if (target === "all") {
|
|
5741
5884
|
const shortName = shortHookName(name);
|
|
5742
|
-
const meta = resolveHookMeta(shortName);
|
|
5885
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
5743
5886
|
if (meta) {
|
|
5744
5887
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
5745
5888
|
if (unsupportedTargets.length > 0) {
|
|
@@ -5772,7 +5915,7 @@ function installHook(name, options = {}) {
|
|
|
5772
5915
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
5773
5916
|
}
|
|
5774
5917
|
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
5775
|
-
const meta = resolveHookMeta(name);
|
|
5918
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
5776
5919
|
if (!meta)
|
|
5777
5920
|
return;
|
|
5778
5921
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -5789,8 +5932,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
5789
5932
|
const settings = readSettings(scope, target);
|
|
5790
5933
|
if (!settings.hooks)
|
|
5791
5934
|
settings.hooks = {};
|
|
5935
|
+
if (isNativeSafetyName(name)) {
|
|
5936
|
+
for (const groups of Object.values(settings.hooks))
|
|
5937
|
+
for (const group of groups)
|
|
5938
|
+
for (const hook of group.hooks ?? []) {
|
|
5939
|
+
if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
|
|
5940
|
+
throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5792
5944
|
const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
|
|
5793
|
-
const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
5945
|
+
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
5794
5946
|
if (target === "codex") {
|
|
5795
5947
|
const positions = [];
|
|
5796
5948
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
@@ -5798,9 +5950,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
5798
5950
|
for (let index = 0;index < entry.hooks.length; index++) {
|
|
5799
5951
|
const command = entry.hooks[index]?.command;
|
|
5800
5952
|
const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
|
|
5801
|
-
if (match && (match[1] ?? match[2]) === name) {
|
|
5953
|
+
if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
|
|
5802
5954
|
if (registrationName(command) !== name)
|
|
5803
5955
|
throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
|
|
5956
|
+
if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
|
|
5957
|
+
throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
|
|
5804
5958
|
positions.push({ event, entry, index });
|
|
5805
5959
|
}
|
|
5806
5960
|
}
|
|
@@ -5864,12 +6018,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
5864
6018
|
if (target === "codewith") {
|
|
5865
6019
|
const config = readCodewithConfig(scope);
|
|
5866
6020
|
const registered2 = [];
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
6021
|
+
try {
|
|
6022
|
+
const parsed = Bun.TOML.parse(config);
|
|
6023
|
+
for (const groups of Object.values(parsed.hooks ?? {})) {
|
|
6024
|
+
if (!Array.isArray(groups))
|
|
6025
|
+
continue;
|
|
6026
|
+
for (const group of groups)
|
|
6027
|
+
for (const hook of group.hooks ?? []) {
|
|
6028
|
+
const name = registrationName(hook.command);
|
|
6029
|
+
if (name)
|
|
6030
|
+
registered2.push(name);
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
} catch {
|
|
6034
|
+
return [];
|
|
5873
6035
|
}
|
|
5874
6036
|
return [...new Set(registered2)];
|
|
5875
6037
|
}
|
|
@@ -5914,9 +6076,9 @@ function removeHookForTarget(name, scope, target) {
|
|
|
5914
6076
|
}
|
|
5915
6077
|
function readSettingsAt(path) {
|
|
5916
6078
|
try {
|
|
5917
|
-
if (!
|
|
6079
|
+
if (!existsSync12(path))
|
|
5918
6080
|
return;
|
|
5919
|
-
const raw =
|
|
6081
|
+
const raw = readFileSync6(path, "utf-8");
|
|
5920
6082
|
return { raw, settings: JSON.parse(raw) };
|
|
5921
6083
|
} catch {
|
|
5922
6084
|
return;
|
|
@@ -5950,7 +6112,7 @@ function pruneStaleRegistrations(scope = "global") {
|
|
|
5950
6112
|
staleCommandsByEvent.set(finding.event, commands);
|
|
5951
6113
|
}
|
|
5952
6114
|
const settings = target === "codex" ? readCodexSettings(path) : parsed.settings;
|
|
5953
|
-
if (target === "codex" &&
|
|
6115
|
+
if (target === "codex" && readFileSync6(path, "utf8") !== parsed.raw)
|
|
5954
6116
|
throw new Error("Codex settings changed before pruning; retry.");
|
|
5955
6117
|
for (const [eventKey, commands] of staleCommandsByEvent) {
|
|
5956
6118
|
const entries = settings.hooks?.[eventKey];
|
|
@@ -6038,10 +6200,10 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
6038
6200
|
}
|
|
6039
6201
|
function codewithHasHookEntry(name, scope = "global") {
|
|
6040
6202
|
const path = getSettingsPath(scope, "codewith");
|
|
6041
|
-
if (!
|
|
6203
|
+
if (!existsSync12(path))
|
|
6042
6204
|
return false;
|
|
6043
6205
|
try {
|
|
6044
|
-
return codewithHasHookEntryInText(
|
|
6206
|
+
return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
|
|
6045
6207
|
} catch {
|
|
6046
6208
|
return false;
|
|
6047
6209
|
}
|
|
@@ -6050,12 +6212,12 @@ function removeStoreDirFailClosed(dir) {
|
|
|
6050
6212
|
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
6051
6213
|
for (const entry of entries) {
|
|
6052
6214
|
if (entry.isDirectory()) {
|
|
6053
|
-
removeStoreDirFailClosed(
|
|
6215
|
+
removeStoreDirFailClosed(join11(dir, entry.name));
|
|
6054
6216
|
}
|
|
6055
6217
|
}
|
|
6056
6218
|
for (const entry of entries) {
|
|
6057
6219
|
if (!entry.isDirectory()) {
|
|
6058
|
-
unlinkSync2(
|
|
6220
|
+
unlinkSync2(join11(dir, entry.name));
|
|
6059
6221
|
}
|
|
6060
6222
|
}
|
|
6061
6223
|
rmdirSync(dir);
|
|
@@ -6086,8 +6248,8 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
6086
6248
|
const registrationsRemaining = [];
|
|
6087
6249
|
if (target === "codewith" || target === "all") {
|
|
6088
6250
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
6089
|
-
if (
|
|
6090
|
-
const before =
|
|
6251
|
+
if (existsSync12(codewithPath)) {
|
|
6252
|
+
const before = readFileSync6(codewithPath, "utf-8");
|
|
6091
6253
|
const after = removeCodewithHookEntry(before, shortName);
|
|
6092
6254
|
if (after.removed) {
|
|
6093
6255
|
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
@@ -6134,8 +6296,9 @@ var init_installer = __esm(() => {
|
|
|
6134
6296
|
init_registration();
|
|
6135
6297
|
init_store();
|
|
6136
6298
|
init_codex_settings();
|
|
6137
|
-
|
|
6138
|
-
|
|
6299
|
+
init_native_safety();
|
|
6300
|
+
__dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
6301
|
+
HOOKS_DIR = existsSync12(join11(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join11(__dirname2, "..", "..", "hooks") : join11(__dirname2, "..", "hooks");
|
|
6139
6302
|
EVENT_MAP = {
|
|
6140
6303
|
claude: {
|
|
6141
6304
|
PreToolUse: "PreToolUse",
|
|
@@ -6248,12 +6411,12 @@ var init_redact = __esm(() => {
|
|
|
6248
6411
|
});
|
|
6249
6412
|
|
|
6250
6413
|
// src/lib/profiles.ts
|
|
6251
|
-
import { existsSync as
|
|
6252
|
-
import { join as
|
|
6414
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
6415
|
+
import { join as join12 } from "path";
|
|
6253
6416
|
function getProfilesDir() {
|
|
6254
|
-
const newDir =
|
|
6255
|
-
const oldDir =
|
|
6256
|
-
if (!
|
|
6417
|
+
const newDir = join12(getEffectiveDataRoot(), "profiles");
|
|
6418
|
+
const oldDir = join12(getHomeDir(), ".hooks", "profiles");
|
|
6419
|
+
if (!existsSync13(newDir) && existsSync13(oldDir)) {
|
|
6257
6420
|
mkdirSync6(newDir, { recursive: true });
|
|
6258
6421
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
6259
6422
|
}
|
|
@@ -6261,12 +6424,12 @@ function getProfilesDir() {
|
|
|
6261
6424
|
}
|
|
6262
6425
|
function ensureProfilesDir() {
|
|
6263
6426
|
const dir = getProfilesDir();
|
|
6264
|
-
if (!
|
|
6427
|
+
if (!existsSync13(dir)) {
|
|
6265
6428
|
mkdirSync6(dir, { recursive: true });
|
|
6266
6429
|
}
|
|
6267
6430
|
}
|
|
6268
6431
|
function profilePath(id) {
|
|
6269
|
-
return
|
|
6432
|
+
return join12(getProfilesDir(), `${id}.json`);
|
|
6270
6433
|
}
|
|
6271
6434
|
function shortUuid() {
|
|
6272
6435
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -6292,23 +6455,23 @@ function createProfile(input) {
|
|
|
6292
6455
|
function getProfile(id) {
|
|
6293
6456
|
const path = profilePath(id);
|
|
6294
6457
|
try {
|
|
6295
|
-
if (!
|
|
6458
|
+
if (!existsSync13(path))
|
|
6296
6459
|
return null;
|
|
6297
|
-
return JSON.parse(
|
|
6460
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
6298
6461
|
} catch {
|
|
6299
6462
|
return null;
|
|
6300
6463
|
}
|
|
6301
6464
|
}
|
|
6302
6465
|
function listProfiles() {
|
|
6303
6466
|
const dir = getProfilesDir();
|
|
6304
|
-
if (!
|
|
6467
|
+
if (!existsSync13(dir))
|
|
6305
6468
|
return [];
|
|
6306
6469
|
try {
|
|
6307
6470
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
6308
6471
|
const profiles = [];
|
|
6309
6472
|
for (const file of files) {
|
|
6310
6473
|
try {
|
|
6311
|
-
const content =
|
|
6474
|
+
const content = readFileSync7(join12(dir, file), "utf-8");
|
|
6312
6475
|
profiles.push(JSON.parse(content));
|
|
6313
6476
|
} catch {}
|
|
6314
6477
|
}
|
|
@@ -6338,7 +6501,7 @@ function importProfiles(profiles) {
|
|
|
6338
6501
|
continue;
|
|
6339
6502
|
}
|
|
6340
6503
|
const path = profilePath(profile.agent_id);
|
|
6341
|
-
if (
|
|
6504
|
+
if (existsSync13(path)) {
|
|
6342
6505
|
skipped++;
|
|
6343
6506
|
continue;
|
|
6344
6507
|
}
|
|
@@ -6355,15 +6518,15 @@ var init_profiles = __esm(() => {
|
|
|
6355
6518
|
// ../contracts/dist/client/transport.js
|
|
6356
6519
|
import { isIP as isIP2 } from "net";
|
|
6357
6520
|
import { spawnSync } from "child_process";
|
|
6358
|
-
import { closeSync as
|
|
6521
|
+
import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
|
|
6359
6522
|
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
6360
6523
|
import { hostname as osHostname } from "os";
|
|
6361
|
-
import { isAbsolute as
|
|
6524
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
6362
6525
|
import { createRequire as createRequire2 } from "module";
|
|
6363
6526
|
import { fileURLToPath as fileURLToPath22 } from "url";
|
|
6364
|
-
import { closeSync as closeSync3, fstatSync, lstatSync as
|
|
6527
|
+
import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
|
|
6365
6528
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
6366
|
-
import { basename as basename2, dirname as
|
|
6529
|
+
import { basename as basename2, dirname as dirname6, join as join13, relative, sep as sep2 } from "path";
|
|
6367
6530
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
6368
6531
|
function envToken(name) {
|
|
6369
6532
|
return name.toUpperCase().replace(/-/g, "_");
|
|
@@ -6437,14 +6600,14 @@ function o(e2, r2, t2) {
|
|
|
6437
6600
|
}
|
|
6438
6601
|
}
|
|
6439
6602
|
function entryPoint(directory) {
|
|
6440
|
-
const root =
|
|
6441
|
-
const fd = openSync3(
|
|
6603
|
+
const root = realpathSync3(directory);
|
|
6604
|
+
const fd = openSync3(join13(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
6442
6605
|
let pkg;
|
|
6443
6606
|
try {
|
|
6444
|
-
const stat =
|
|
6607
|
+
const stat = fstatSync3(fd);
|
|
6445
6608
|
if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES2)
|
|
6446
6609
|
throw new Error("Invalid Secrets package metadata");
|
|
6447
|
-
pkg = JSON.parse(
|
|
6610
|
+
pkg = JSON.parse(readFileSync8(fd, "utf8"));
|
|
6448
6611
|
} finally {
|
|
6449
6612
|
closeSync3(fd);
|
|
6450
6613
|
}
|
|
@@ -6458,7 +6621,7 @@ function entryPoint(directory) {
|
|
|
6458
6621
|
if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
|
|
6459
6622
|
throw new Error("Invalid Secrets import export");
|
|
6460
6623
|
}
|
|
6461
|
-
const file =
|
|
6624
|
+
const file = realpathSync3(join13(root, ...parts));
|
|
6462
6625
|
const within = relative(root, file);
|
|
6463
6626
|
if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
|
|
6464
6627
|
throw new Error("Secrets import export escapes its package or is not a file");
|
|
@@ -6466,13 +6629,13 @@ function entryPoint(directory) {
|
|
|
6466
6629
|
return pathToFileURL(file).href;
|
|
6467
6630
|
}
|
|
6468
6631
|
function resolveInstalledSecrets(parentUrl) {
|
|
6469
|
-
let directory =
|
|
6632
|
+
let directory = dirname6(fileURLToPath3(parentUrl));
|
|
6470
6633
|
for (;; ) {
|
|
6471
6634
|
if (basename2(directory) !== "node_modules") {
|
|
6472
|
-
const candidate =
|
|
6635
|
+
const candidate = join13(directory, "node_modules", PACKAGE);
|
|
6473
6636
|
let absent = false;
|
|
6474
6637
|
try {
|
|
6475
|
-
|
|
6638
|
+
lstatSync3(candidate);
|
|
6476
6639
|
} catch (error) {
|
|
6477
6640
|
if (error.code !== "ENOENT")
|
|
6478
6641
|
throw error;
|
|
@@ -6481,7 +6644,7 @@ function resolveInstalledSecrets(parentUrl) {
|
|
|
6481
6644
|
if (!absent)
|
|
6482
6645
|
return entryPoint(candidate);
|
|
6483
6646
|
}
|
|
6484
|
-
const parent =
|
|
6647
|
+
const parent = dirname6(directory);
|
|
6485
6648
|
if (parent === directory)
|
|
6486
6649
|
throw new Error("Secrets SDK is not installed for this consumer");
|
|
6487
6650
|
directory = parent;
|
|
@@ -6493,7 +6656,7 @@ function homeDir(env) {
|
|
|
6493
6656
|
}
|
|
6494
6657
|
function absoluteOverride(env, key) {
|
|
6495
6658
|
const value = env[key]?.trim();
|
|
6496
|
-
return value &&
|
|
6659
|
+
return value && isAbsolute3(value) ? value : null;
|
|
6497
6660
|
}
|
|
6498
6661
|
function hasnaHomeDir(env) {
|
|
6499
6662
|
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
@@ -6539,9 +6702,9 @@ function parseEnvFile(text) {
|
|
|
6539
6702
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
6540
6703
|
continue;
|
|
6541
6704
|
let value = withoutExport.slice(equals + 1).trim();
|
|
6542
|
-
const
|
|
6543
|
-
if (
|
|
6544
|
-
if (value.length < 2 || !value.endsWith(
|
|
6705
|
+
const quote2 = value[0];
|
|
6706
|
+
if (quote2 === '"' || quote2 === "'") {
|
|
6707
|
+
if (value.length < 2 || !value.endsWith(quote2)) {
|
|
6545
6708
|
unusable.add(key);
|
|
6546
6709
|
continue;
|
|
6547
6710
|
}
|
|
@@ -6570,7 +6733,7 @@ function readAppConfigFile(path) {
|
|
|
6570
6733
|
};
|
|
6571
6734
|
let fd = -1;
|
|
6572
6735
|
try {
|
|
6573
|
-
fd =
|
|
6736
|
+
fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
6574
6737
|
} catch (error) {
|
|
6575
6738
|
const code = error.code;
|
|
6576
6739
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -6599,7 +6762,7 @@ function readAppConfigFile(path) {
|
|
|
6599
6762
|
return parseEnvFile(bytes.toString("utf8"));
|
|
6600
6763
|
} finally {
|
|
6601
6764
|
if (fd !== -1)
|
|
6602
|
-
|
|
6765
|
+
closeSync22(fd);
|
|
6603
6766
|
}
|
|
6604
6767
|
}
|
|
6605
6768
|
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
@@ -7600,12 +7763,12 @@ var init_db_writer = __esm(() => {
|
|
|
7600
7763
|
});
|
|
7601
7764
|
|
|
7602
7765
|
// src/lib/hook-env.ts
|
|
7603
|
-
import { dirname as
|
|
7604
|
-
import { existsSync as
|
|
7766
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4 } from "path";
|
|
7767
|
+
import { existsSync as existsSync14, realpathSync as realpathSync4, statSync as statSync2 } from "fs";
|
|
7605
7768
|
import { tmpdir } from "os";
|
|
7606
7769
|
function isDeniedEnvName(name) {
|
|
7607
7770
|
const upper = name.toUpperCase();
|
|
7608
|
-
if (DENY_PREFIXES.some((
|
|
7771
|
+
if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
|
|
7609
7772
|
return true;
|
|
7610
7773
|
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
7611
7774
|
return true;
|
|
@@ -7614,13 +7777,13 @@ function isDeniedEnvName(name) {
|
|
|
7614
7777
|
function isInterpreterInjectionEnvName(name) {
|
|
7615
7778
|
if (INTERPRETER_INJECTION.has(name))
|
|
7616
7779
|
return true;
|
|
7617
|
-
return INTERPRETER_INJECTION_PREFIXES.some((
|
|
7780
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
|
|
7618
7781
|
}
|
|
7619
7782
|
function isBashFunctionEnvName(name) {
|
|
7620
7783
|
return name.startsWith("BASH_FUNC_");
|
|
7621
7784
|
}
|
|
7622
7785
|
function isUnsafePathEntry(entry, home) {
|
|
7623
|
-
if (entry === "" || !
|
|
7786
|
+
if (entry === "" || !isAbsolute4(entry))
|
|
7624
7787
|
return true;
|
|
7625
7788
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
7626
7789
|
return true;
|
|
@@ -7630,7 +7793,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7630
7793
|
return true;
|
|
7631
7794
|
const canonical = (path) => {
|
|
7632
7795
|
try {
|
|
7633
|
-
return
|
|
7796
|
+
return realpathSync4(path);
|
|
7634
7797
|
} catch {
|
|
7635
7798
|
return path;
|
|
7636
7799
|
}
|
|
@@ -7649,9 +7812,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7649
7812
|
}
|
|
7650
7813
|
function sanitizeHookPath(pathValue, home) {
|
|
7651
7814
|
const baseline = PATH_BASELINE.slice();
|
|
7652
|
-
if (process.platform === "darwin" &&
|
|
7815
|
+
if (process.platform === "darwin" && existsSync14("/opt/homebrew/bin"))
|
|
7653
7816
|
baseline.push("/opt/homebrew/bin");
|
|
7654
|
-
const bunDir =
|
|
7817
|
+
const bunDir = dirname7(process.execPath);
|
|
7655
7818
|
const seen = new Set;
|
|
7656
7819
|
const out = [];
|
|
7657
7820
|
for (const dir of baseline) {
|
|
@@ -7776,7 +7939,7 @@ __export(exports_run, {
|
|
|
7776
7939
|
});
|
|
7777
7940
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
7778
7941
|
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
7779
|
-
import { basename as basename3, dirname as
|
|
7942
|
+
import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute5, join as join14 } from "path";
|
|
7780
7943
|
function extensionOf(scriptPath) {
|
|
7781
7944
|
const base = basename3(scriptPath);
|
|
7782
7945
|
const dot = base.lastIndexOf(".");
|
|
@@ -7845,9 +8008,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
7845
8008
|
return chunks.join("");
|
|
7846
8009
|
}
|
|
7847
8010
|
async function executeVerifiedScript(options) {
|
|
7848
|
-
const scriptDir =
|
|
8011
|
+
const scriptDir = dirname8(options.scriptPath);
|
|
7849
8012
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
7850
|
-
const tempPath =
|
|
8013
|
+
const tempPath = join14(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
7851
8014
|
let proc = null;
|
|
7852
8015
|
let timedOut = false;
|
|
7853
8016
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -7856,7 +8019,7 @@ async function executeVerifiedScript(options) {
|
|
|
7856
8019
|
}, options.timeout) : null;
|
|
7857
8020
|
try {
|
|
7858
8021
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
7859
|
-
proc = Bun.spawn([...interpreter.command,
|
|
8022
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
7860
8023
|
stdin: new Response(options.stdin),
|
|
7861
8024
|
stdout: "pipe",
|
|
7862
8025
|
stderr: "pipe",
|
|
@@ -7938,15 +8101,15 @@ __export(exports_custom_install, {
|
|
|
7938
8101
|
installCustomSource: () => installCustomSource,
|
|
7939
8102
|
ensureCustomDir: () => ensureCustomDir
|
|
7940
8103
|
});
|
|
7941
|
-
import { existsSync as
|
|
7942
|
-
import { join as
|
|
8104
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
|
|
8105
|
+
import { join as join15 } from "path";
|
|
7943
8106
|
import { tmpdir as tmpdir2 } from "os";
|
|
7944
8107
|
function manifestAt(dir) {
|
|
7945
|
-
const path =
|
|
7946
|
-
if (!
|
|
8108
|
+
const path = join15(dir, "manifest.json");
|
|
8109
|
+
if (!existsSync15(path))
|
|
7947
8110
|
return;
|
|
7948
8111
|
try {
|
|
7949
|
-
return parseManifest(
|
|
8112
|
+
return parseManifest(readFileSync9(path, "utf-8"));
|
|
7950
8113
|
} catch {
|
|
7951
8114
|
return;
|
|
7952
8115
|
}
|
|
@@ -7965,8 +8128,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7965
8128
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7966
8129
|
}
|
|
7967
8130
|
async function cloneToTemp(gitUrl) {
|
|
7968
|
-
const tmp = mkdtempSync(
|
|
7969
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
8131
|
+
const tmp = mkdtempSync(join15(tmpdir2(), "hooks-install-"));
|
|
8132
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join15(tmp, "repo")], {
|
|
7970
8133
|
stdout: "pipe",
|
|
7971
8134
|
stderr: "pipe"
|
|
7972
8135
|
});
|
|
@@ -7979,7 +8142,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
7979
8142
|
rmSync3(tmp, { recursive: true, force: true });
|
|
7980
8143
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
7981
8144
|
}
|
|
7982
|
-
return
|
|
8145
|
+
return join15(tmp, "repo");
|
|
7983
8146
|
}
|
|
7984
8147
|
function isGitUrl(value) {
|
|
7985
8148
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -7989,7 +8152,7 @@ function isCustomSource(value) {
|
|
|
7989
8152
|
return true;
|
|
7990
8153
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
7991
8154
|
return true;
|
|
7992
|
-
return
|
|
8155
|
+
return existsSync15(value);
|
|
7993
8156
|
}
|
|
7994
8157
|
async function installCustomSource(source) {
|
|
7995
8158
|
if (isGitUrl(source)) {
|
|
@@ -8003,11 +8166,11 @@ async function installCustomSource(source) {
|
|
|
8003
8166
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
8004
8167
|
return installManifestUrl(source);
|
|
8005
8168
|
}
|
|
8006
|
-
if (!
|
|
8169
|
+
if (!existsSync15(source)) {
|
|
8007
8170
|
throw new Error(`No such local path: ${source}`);
|
|
8008
8171
|
}
|
|
8009
8172
|
if (source.endsWith("manifest.json")) {
|
|
8010
|
-
const manifest = parseManifest(
|
|
8173
|
+
const manifest = parseManifest(readFileSync9(source, "utf-8"));
|
|
8011
8174
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
8012
8175
|
}
|
|
8013
8176
|
return copyManifestDir(source, source, "local");
|
|
@@ -8056,13 +8219,13 @@ __export(exports_sync, {
|
|
|
8056
8219
|
fetchPinnedHook: () => fetchPinnedHook,
|
|
8057
8220
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
8058
8221
|
});
|
|
8059
|
-
import { createHash as
|
|
8060
|
-
import { readFileSync as
|
|
8222
|
+
import { createHash as createHash3 } from "crypto";
|
|
8223
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
8061
8224
|
function sha256OfText(text) {
|
|
8062
|
-
return
|
|
8225
|
+
return createHash3("sha256").update(text).digest("hex");
|
|
8063
8226
|
}
|
|
8064
8227
|
function sha256OfPath(path) {
|
|
8065
|
-
return
|
|
8228
|
+
return createHash3("sha256").update(readFileSync10(path)).digest("hex");
|
|
8066
8229
|
}
|
|
8067
8230
|
function collectBundledCatalog() {
|
|
8068
8231
|
const out = [];
|
|
@@ -8856,13 +9019,12 @@ function secureEqual(a, b) {
|
|
|
8856
9019
|
}
|
|
8857
9020
|
|
|
8858
9021
|
// src/openapi.ts
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
openApiDocument = {
|
|
9022
|
+
function buildOpenApiDocument(version) {
|
|
9023
|
+
return {
|
|
8862
9024
|
openapi: "3.1.0",
|
|
8863
9025
|
info: {
|
|
8864
9026
|
title: "Hooks Registry API",
|
|
8865
|
-
version
|
|
9027
|
+
version,
|
|
8866
9028
|
description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
|
|
8867
9029
|
},
|
|
8868
9030
|
servers: [{ url: "/" }],
|
|
@@ -8876,7 +9038,7 @@ var init_openapi = __esm(() => {
|
|
|
8876
9038
|
description: "Service is alive",
|
|
8877
9039
|
content: {
|
|
8878
9040
|
"application/json": {
|
|
8879
|
-
schema: {
|
|
9041
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
8880
9042
|
}
|
|
8881
9043
|
}
|
|
8882
9044
|
}
|
|
@@ -8888,8 +9050,22 @@ var init_openapi = __esm(() => {
|
|
|
8888
9050
|
summary: "Readiness probe \u2014 the hooks.lock store is readable",
|
|
8889
9051
|
operationId: "getReady",
|
|
8890
9052
|
responses: {
|
|
8891
|
-
"200": {
|
|
8892
|
-
|
|
9053
|
+
"200": {
|
|
9054
|
+
description: "Lock store is readable",
|
|
9055
|
+
content: {
|
|
9056
|
+
"application/json": {
|
|
9057
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
},
|
|
9061
|
+
"503": {
|
|
9062
|
+
description: "Lock store is not readable",
|
|
9063
|
+
content: {
|
|
9064
|
+
"application/json": {
|
|
9065
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
9066
|
+
}
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
8893
9069
|
}
|
|
8894
9070
|
}
|
|
8895
9071
|
},
|
|
@@ -8902,7 +9078,7 @@ var init_openapi = __esm(() => {
|
|
|
8902
9078
|
description: "The installed @hasna/hooks version",
|
|
8903
9079
|
content: {
|
|
8904
9080
|
"application/json": {
|
|
8905
|
-
schema: {
|
|
9081
|
+
schema: { $ref: "#/components/schemas/HooksVersion" }
|
|
8906
9082
|
}
|
|
8907
9083
|
}
|
|
8908
9084
|
}
|
|
@@ -8913,7 +9089,16 @@ var init_openapi = __esm(() => {
|
|
|
8913
9089
|
get: {
|
|
8914
9090
|
summary: "This OpenAPI document",
|
|
8915
9091
|
operationId: "getOpenApi",
|
|
8916
|
-
responses: {
|
|
9092
|
+
responses: {
|
|
9093
|
+
"200": {
|
|
9094
|
+
description: "OpenAPI 3.1 document",
|
|
9095
|
+
content: {
|
|
9096
|
+
"application/json": {
|
|
9097
|
+
schema: { type: "object", additionalProperties: true }
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
}
|
|
9101
|
+
}
|
|
8917
9102
|
}
|
|
8918
9103
|
},
|
|
8919
9104
|
"/api/v1/catalog": {
|
|
@@ -8925,23 +9110,7 @@ var init_openapi = __esm(() => {
|
|
|
8925
9110
|
description: "Catalog entries",
|
|
8926
9111
|
content: {
|
|
8927
9112
|
"application/json": {
|
|
8928
|
-
schema: {
|
|
8929
|
-
type: "object",
|
|
8930
|
-
properties: {
|
|
8931
|
-
hooks: {
|
|
8932
|
-
type: "array",
|
|
8933
|
-
items: {
|
|
8934
|
-
type: "object",
|
|
8935
|
-
properties: {
|
|
8936
|
-
name: { type: "string" },
|
|
8937
|
-
version: { type: "string" },
|
|
8938
|
-
sha256: { type: "string" },
|
|
8939
|
-
events: { type: "array", items: { type: "string" } }
|
|
8940
|
-
}
|
|
8941
|
-
}
|
|
8942
|
-
}
|
|
8943
|
-
}
|
|
8944
|
-
}
|
|
9113
|
+
schema: { $ref: "#/components/schemas/HooksCatalog" }
|
|
8945
9114
|
}
|
|
8946
9115
|
}
|
|
8947
9116
|
}
|
|
@@ -8952,7 +9121,16 @@ var init_openapi = __esm(() => {
|
|
|
8952
9121
|
get: {
|
|
8953
9122
|
summary: "Read the published hooks.lock with per-hook versions",
|
|
8954
9123
|
operationId: "getLock",
|
|
8955
|
-
responses: {
|
|
9124
|
+
responses: {
|
|
9125
|
+
"200": {
|
|
9126
|
+
description: "The lock file",
|
|
9127
|
+
content: {
|
|
9128
|
+
"application/json": {
|
|
9129
|
+
schema: { $ref: "#/components/schemas/HooksLock" }
|
|
9130
|
+
}
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
8956
9134
|
}
|
|
8957
9135
|
},
|
|
8958
9136
|
"/api/v1/hooks/{name}/{version}": {
|
|
@@ -8965,10 +9143,35 @@ var init_openapi = __esm(() => {
|
|
|
8965
9143
|
],
|
|
8966
9144
|
responses: {
|
|
8967
9145
|
"200": {
|
|
8968
|
-
description: "Hook artifact; x-hook-sha256 header carries the script hash"
|
|
9146
|
+
description: "Hook artifact; x-hook-sha256 header carries the script hash",
|
|
9147
|
+
headers: {
|
|
9148
|
+
"x-hook-sha256": {
|
|
9149
|
+
description: "sha256 of the served script, the same value the lock pins",
|
|
9150
|
+
schema: { type: "string" }
|
|
9151
|
+
}
|
|
9152
|
+
},
|
|
9153
|
+
content: {
|
|
9154
|
+
"application/json": {
|
|
9155
|
+
schema: { $ref: "#/components/schemas/HooksArtifact" }
|
|
9156
|
+
}
|
|
9157
|
+
}
|
|
8969
9158
|
},
|
|
8970
|
-
"400": {
|
|
8971
|
-
|
|
9159
|
+
"400": {
|
|
9160
|
+
description: "Invalid URL encoding or semver version",
|
|
9161
|
+
content: {
|
|
9162
|
+
"application/json": {
|
|
9163
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
9166
|
+
},
|
|
9167
|
+
"404": {
|
|
9168
|
+
description: "Hook not found locally",
|
|
9169
|
+
content: {
|
|
9170
|
+
"application/json": {
|
|
9171
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
8972
9175
|
}
|
|
8973
9176
|
}
|
|
8974
9177
|
},
|
|
@@ -8980,18 +9183,51 @@ var init_openapi = __esm(() => {
|
|
|
8980
9183
|
required: true,
|
|
8981
9184
|
content: {
|
|
8982
9185
|
"application/json": {
|
|
8983
|
-
schema: {
|
|
8984
|
-
type: "object",
|
|
8985
|
-
properties: { name: { type: "string" }, version: { type: "string" } }
|
|
8986
|
-
}
|
|
9186
|
+
schema: { $ref: "#/components/schemas/HooksPublishRequest" }
|
|
8987
9187
|
}
|
|
8988
9188
|
}
|
|
8989
9189
|
},
|
|
8990
9190
|
responses: {
|
|
8991
|
-
"200": {
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
9191
|
+
"200": {
|
|
9192
|
+
description: "Hook retrusted and pin updated",
|
|
9193
|
+
content: {
|
|
9194
|
+
"application/json": {
|
|
9195
|
+
schema: { $ref: "#/components/schemas/HooksPublishResponse" }
|
|
9196
|
+
}
|
|
9197
|
+
}
|
|
9198
|
+
},
|
|
9199
|
+
"400": {
|
|
9200
|
+
description: "Invalid JSON body",
|
|
9201
|
+
content: {
|
|
9202
|
+
"application/json": {
|
|
9203
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9204
|
+
}
|
|
9205
|
+
}
|
|
9206
|
+
},
|
|
9207
|
+
"401": {
|
|
9208
|
+
description: "Missing or invalid API key",
|
|
9209
|
+
content: {
|
|
9210
|
+
"application/json": {
|
|
9211
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9212
|
+
}
|
|
9213
|
+
}
|
|
9214
|
+
},
|
|
9215
|
+
"404": {
|
|
9216
|
+
description: "Hook not found in local store",
|
|
9217
|
+
content: {
|
|
9218
|
+
"application/json": {
|
|
9219
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
},
|
|
9223
|
+
"409": {
|
|
9224
|
+
description: "Version mismatch with the local store",
|
|
9225
|
+
content: {
|
|
9226
|
+
"application/json": {
|
|
9227
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9228
|
+
}
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
8995
9231
|
},
|
|
8996
9232
|
security: [{ apiKey: [] }]
|
|
8997
9233
|
}
|
|
@@ -9004,10 +9240,135 @@ var init_openapi = __esm(() => {
|
|
|
9004
9240
|
scheme: "bearer",
|
|
9005
9241
|
description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
|
|
9006
9242
|
}
|
|
9243
|
+
},
|
|
9244
|
+
schemas: {
|
|
9245
|
+
HooksHealth: {
|
|
9246
|
+
type: "object",
|
|
9247
|
+
additionalProperties: false,
|
|
9248
|
+
required: ["status", "name"],
|
|
9249
|
+
properties: {
|
|
9250
|
+
status: { type: "string" },
|
|
9251
|
+
name: { type: "string" }
|
|
9252
|
+
}
|
|
9253
|
+
},
|
|
9254
|
+
HooksVersion: {
|
|
9255
|
+
type: "object",
|
|
9256
|
+
additionalProperties: false,
|
|
9257
|
+
required: ["version"],
|
|
9258
|
+
properties: { version: { type: "string" } }
|
|
9259
|
+
},
|
|
9260
|
+
HooksCatalogEntry: {
|
|
9261
|
+
type: "object",
|
|
9262
|
+
additionalProperties: false,
|
|
9263
|
+
required: ["name", "version", "sha256", "events", "description", "source", "versions"],
|
|
9264
|
+
properties: {
|
|
9265
|
+
name: { type: "string" },
|
|
9266
|
+
version: { type: "string" },
|
|
9267
|
+
sha256: { type: "string" },
|
|
9268
|
+
events: { type: "array", items: { type: "string" } },
|
|
9269
|
+
description: { type: "string" },
|
|
9270
|
+
source: {
|
|
9271
|
+
type: "string",
|
|
9272
|
+
description: "bundled | custom | custom-overrides-bundled"
|
|
9273
|
+
},
|
|
9274
|
+
versions: { type: "array", items: { type: "string" } }
|
|
9275
|
+
}
|
|
9276
|
+
},
|
|
9277
|
+
HooksCatalog: {
|
|
9278
|
+
type: "object",
|
|
9279
|
+
additionalProperties: false,
|
|
9280
|
+
required: ["hooks"],
|
|
9281
|
+
properties: {
|
|
9282
|
+
hooks: { type: "array", items: { $ref: "#/components/schemas/HooksCatalogEntry" } }
|
|
9283
|
+
}
|
|
9284
|
+
},
|
|
9285
|
+
HooksLockEntry: {
|
|
9286
|
+
type: "object",
|
|
9287
|
+
additionalProperties: false,
|
|
9288
|
+
required: ["version", "sha256", "source"],
|
|
9289
|
+
properties: {
|
|
9290
|
+
version: { type: "string" },
|
|
9291
|
+
sha256: { type: "string" },
|
|
9292
|
+
source: { type: "string" },
|
|
9293
|
+
pinned: {
|
|
9294
|
+
type: "boolean",
|
|
9295
|
+
description: "True for an explicit install/update pin, absent for sync-maintained pins."
|
|
9296
|
+
},
|
|
9297
|
+
versions: { type: "array", items: { type: "string" } }
|
|
9298
|
+
}
|
|
9299
|
+
},
|
|
9300
|
+
HooksLock: {
|
|
9301
|
+
type: "object",
|
|
9302
|
+
additionalProperties: false,
|
|
9303
|
+
required: ["hooks"],
|
|
9304
|
+
properties: {
|
|
9305
|
+
hooks: {
|
|
9306
|
+
type: "object",
|
|
9307
|
+
additionalProperties: { $ref: "#/components/schemas/HooksLockEntry" }
|
|
9308
|
+
}
|
|
9309
|
+
}
|
|
9310
|
+
},
|
|
9311
|
+
HooksManifest: {
|
|
9312
|
+
type: "object",
|
|
9313
|
+
additionalProperties: false,
|
|
9314
|
+
required: ["name", "version", "events", "script"],
|
|
9315
|
+
properties: {
|
|
9316
|
+
name: { type: "string" },
|
|
9317
|
+
version: { type: "string" },
|
|
9318
|
+
description: { type: "string" },
|
|
9319
|
+
events: { type: "array", items: { type: "string" } },
|
|
9320
|
+
script: { type: "string" },
|
|
9321
|
+
script_kind: { type: "string", enum: ["inline", "file"] },
|
|
9322
|
+
args: { type: "array", items: { type: "string" } },
|
|
9323
|
+
timeout_ms: { type: "number" }
|
|
9324
|
+
}
|
|
9325
|
+
},
|
|
9326
|
+
HooksArtifact: {
|
|
9327
|
+
type: "object",
|
|
9328
|
+
additionalProperties: false,
|
|
9329
|
+
required: ["manifest", "script"],
|
|
9330
|
+
properties: {
|
|
9331
|
+
manifest: { $ref: "#/components/schemas/HooksManifest" },
|
|
9332
|
+
script: { type: "string" }
|
|
9333
|
+
}
|
|
9334
|
+
},
|
|
9335
|
+
HooksError: {
|
|
9336
|
+
type: "object",
|
|
9337
|
+
additionalProperties: false,
|
|
9338
|
+
required: ["error"],
|
|
9339
|
+
properties: { error: { type: "string" } }
|
|
9340
|
+
},
|
|
9341
|
+
HooksPublishRequest: {
|
|
9342
|
+
type: "object",
|
|
9343
|
+
additionalProperties: false,
|
|
9344
|
+
required: ["name"],
|
|
9345
|
+
properties: {
|
|
9346
|
+
name: { type: "string" },
|
|
9347
|
+
version: { type: "string" }
|
|
9348
|
+
}
|
|
9349
|
+
},
|
|
9350
|
+
HooksPublishResponse: {
|
|
9351
|
+
type: "object",
|
|
9352
|
+
additionalProperties: false,
|
|
9353
|
+
required: ["ok", "hook"],
|
|
9354
|
+
properties: {
|
|
9355
|
+
ok: { type: "boolean" },
|
|
9356
|
+
hook: {
|
|
9357
|
+
type: "object",
|
|
9358
|
+
additionalProperties: false,
|
|
9359
|
+
required: ["name", "version", "sha256"],
|
|
9360
|
+
properties: {
|
|
9361
|
+
name: { type: "string" },
|
|
9362
|
+
version: { type: "string" },
|
|
9363
|
+
sha256: { type: "string" }
|
|
9364
|
+
}
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
9367
|
+
}
|
|
9007
9368
|
}
|
|
9008
9369
|
}
|
|
9009
9370
|
};
|
|
9010
|
-
}
|
|
9371
|
+
}
|
|
9011
9372
|
|
|
9012
9373
|
// src/serve.ts
|
|
9013
9374
|
var exports_serve = {};
|
|
@@ -9019,13 +9380,13 @@ __export(exports_serve, {
|
|
|
9019
9380
|
SERVE_HOST: () => SERVE_HOST,
|
|
9020
9381
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
9021
9382
|
});
|
|
9022
|
-
import { readFileSync as
|
|
9023
|
-
import { dirname as
|
|
9383
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
9384
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
9024
9385
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
9025
9386
|
function packageVersion() {
|
|
9026
9387
|
try {
|
|
9027
|
-
const pkgPath =
|
|
9028
|
-
return JSON.parse(
|
|
9388
|
+
const pkgPath = join16(dirname9(fileURLToPath4(import.meta.url)), "..", "package.json");
|
|
9389
|
+
return JSON.parse(readFileSync11(pkgPath, "utf-8")).version || "0.0.0";
|
|
9029
9390
|
} catch {
|
|
9030
9391
|
return "0.0.0";
|
|
9031
9392
|
}
|
|
@@ -9114,7 +9475,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
9114
9475
|
return Promise.resolve(json({ version: packageVersion() }));
|
|
9115
9476
|
}
|
|
9116
9477
|
if (url.pathname === "/openapi.json" && req.method === "GET") {
|
|
9117
|
-
return Promise.resolve(json(
|
|
9478
|
+
return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
|
|
9118
9479
|
}
|
|
9119
9480
|
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
9120
9481
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
@@ -9199,7 +9560,6 @@ var init_serve = __esm(() => {
|
|
|
9199
9560
|
init_store();
|
|
9200
9561
|
init_resolve();
|
|
9201
9562
|
init_transport2();
|
|
9202
|
-
init_openapi();
|
|
9203
9563
|
init_semver();
|
|
9204
9564
|
if (false) {}
|
|
9205
9565
|
});
|
|
@@ -9326,17 +9686,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
9326
9686
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9327
9687
|
import { z as z2 } from "zod";
|
|
9328
9688
|
import { createServer } from "http";
|
|
9329
|
-
import { existsSync as
|
|
9330
|
-
import { join as
|
|
9689
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
|
|
9690
|
+
import { join as join17, dirname as dirname10 } from "path";
|
|
9331
9691
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
9332
9692
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
9333
9693
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
9334
9694
|
const resolved = resolveHook2(name);
|
|
9335
9695
|
const hookScript = resolveScriptPath2(name);
|
|
9336
|
-
if (!resolved || !hookScript || !
|
|
9696
|
+
if (!resolved || !hookScript || !existsSync16(hookScript)) {
|
|
9337
9697
|
throw new Error(`Hook '${name}' not found`);
|
|
9338
9698
|
}
|
|
9339
|
-
const content =
|
|
9699
|
+
const content = readFileSync12(hookScript);
|
|
9340
9700
|
const sha = sha256Of(content);
|
|
9341
9701
|
const check = checkScriptHash(name, sha);
|
|
9342
9702
|
if (!check.ok) {
|
|
@@ -9541,7 +9901,7 @@ function createHooksServer() {
|
|
|
9541
9901
|
const settingsPath = getSettingsPath(scope);
|
|
9542
9902
|
const issues = [];
|
|
9543
9903
|
const healthy = [];
|
|
9544
|
-
const settingsExist =
|
|
9904
|
+
const settingsExist = existsSync16(settingsPath);
|
|
9545
9905
|
if (!settingsExist) {
|
|
9546
9906
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
9547
9907
|
}
|
|
@@ -9551,7 +9911,7 @@ function createHooksServer() {
|
|
|
9551
9911
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
9552
9912
|
let hookHealthy = true;
|
|
9553
9913
|
if (custom) {
|
|
9554
|
-
if (!
|
|
9914
|
+
if (!existsSync16(custom.scriptPath)) {
|
|
9555
9915
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
9556
9916
|
hookHealthy = false;
|
|
9557
9917
|
}
|
|
@@ -9561,14 +9921,14 @@ function createHooksServer() {
|
|
|
9561
9921
|
continue;
|
|
9562
9922
|
}
|
|
9563
9923
|
const hookDir = getHookPath(name);
|
|
9564
|
-
if (!
|
|
9924
|
+
if (!existsSync16(join17(hookDir, "src", "hook.ts"))) {
|
|
9565
9925
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
9566
9926
|
hookHealthy = false;
|
|
9567
9927
|
}
|
|
9568
9928
|
}
|
|
9569
9929
|
if (meta && settingsExist) {
|
|
9570
9930
|
try {
|
|
9571
|
-
const settings = JSON.parse(
|
|
9931
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
9572
9932
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
9573
9933
|
const eventName = meta.event.split(":")[0];
|
|
9574
9934
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -9605,10 +9965,10 @@ function createHooksServer() {
|
|
|
9605
9965
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
9606
9966
|
}
|
|
9607
9967
|
const hookPath = getHookPath(name);
|
|
9608
|
-
const readmePath =
|
|
9968
|
+
const readmePath = join17(hookPath, "README.md");
|
|
9609
9969
|
let readme = "";
|
|
9610
|
-
if (
|
|
9611
|
-
readme =
|
|
9970
|
+
if (existsSync16(readmePath)) {
|
|
9971
|
+
readme = readFileSync12(readmePath, "utf-8");
|
|
9612
9972
|
}
|
|
9613
9973
|
if (verbose) {
|
|
9614
9974
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -9791,7 +10151,7 @@ function createHooksServer() {
|
|
|
9791
10151
|
const ctx = {
|
|
9792
10152
|
scope,
|
|
9793
10153
|
settings_path: settingsPath,
|
|
9794
|
-
settings_exists:
|
|
10154
|
+
settings_exists: existsSync16(settingsPath),
|
|
9795
10155
|
registered_hooks: hooks,
|
|
9796
10156
|
hook_count: hooks.length,
|
|
9797
10157
|
healthy,
|
|
@@ -9937,8 +10297,8 @@ function createHooksServer() {
|
|
|
9937
10297
|
const settingsPath = getSettingsPath(scope);
|
|
9938
10298
|
let settings = {};
|
|
9939
10299
|
try {
|
|
9940
|
-
if (
|
|
9941
|
-
settings = JSON.parse(
|
|
10300
|
+
if (existsSync16(settingsPath))
|
|
10301
|
+
settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
9942
10302
|
} catch {}
|
|
9943
10303
|
if (!settings.hooks)
|
|
9944
10304
|
settings.hooks = {};
|
|
@@ -9947,8 +10307,8 @@ function createHooksServer() {
|
|
|
9947
10307
|
disabled.push(name);
|
|
9948
10308
|
settings.hooks.__disabled = disabled;
|
|
9949
10309
|
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
9950
|
-
const { dirname:
|
|
9951
|
-
mkdirSync8(
|
|
10310
|
+
const { dirname: dirname11 } = await import("path");
|
|
10311
|
+
mkdirSync8(dirname11(settingsPath), { recursive: true });
|
|
9952
10312
|
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
9953
10313
|
`);
|
|
9954
10314
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -9960,8 +10320,8 @@ function createHooksServer() {
|
|
|
9960
10320
|
const settingsPath = getSettingsPath(scope);
|
|
9961
10321
|
let settings = {};
|
|
9962
10322
|
try {
|
|
9963
|
-
if (
|
|
9964
|
-
settings = JSON.parse(
|
|
10323
|
+
if (existsSync16(settingsPath))
|
|
10324
|
+
settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
9965
10325
|
} catch {}
|
|
9966
10326
|
if (settings.hooks?.__disabled) {
|
|
9967
10327
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n2) => n2 !== name);
|
|
@@ -10331,13 +10691,13 @@ var init_server = __esm(() => {
|
|
|
10331
10691
|
init_redact();
|
|
10332
10692
|
init_authority();
|
|
10333
10693
|
init_storage();
|
|
10334
|
-
__dirname3 =
|
|
10694
|
+
__dirname3 = dirname10(fileURLToPath5(import.meta.url));
|
|
10335
10695
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
10336
10696
|
try {
|
|
10337
10697
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
10338
|
-
const p =
|
|
10339
|
-
if (
|
|
10340
|
-
pkg = JSON.parse(
|
|
10698
|
+
const p = join17(__dirname3, rel);
|
|
10699
|
+
if (existsSync16(p)) {
|
|
10700
|
+
pkg = JSON.parse(readFileSync12(p, "utf-8"));
|
|
10341
10701
|
break;
|
|
10342
10702
|
}
|
|
10343
10703
|
}
|
|
@@ -12079,8 +12439,8 @@ var {
|
|
|
12079
12439
|
|
|
12080
12440
|
// src/cli/index.tsx
|
|
12081
12441
|
import chalk2 from "chalk";
|
|
12082
|
-
import { existsSync as
|
|
12083
|
-
import { join as
|
|
12442
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, rmSync as rmSync4 } from "fs";
|
|
12443
|
+
import { join as join18, dirname as dirname11 } from "path";
|
|
12084
12444
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
12085
12445
|
|
|
12086
12446
|
// src/cli/components/App.tsx
|
|
@@ -13253,6 +13613,7 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
13253
13613
|
}
|
|
13254
13614
|
|
|
13255
13615
|
// src/cli/index.tsx
|
|
13616
|
+
init_native_safety();
|
|
13256
13617
|
init_registry();
|
|
13257
13618
|
init_installer();
|
|
13258
13619
|
init_registration();
|
|
@@ -13268,10 +13629,10 @@ init_local_opt_in();
|
|
|
13268
13629
|
init_transport2();
|
|
13269
13630
|
init_db();
|
|
13270
13631
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
13271
|
-
var __dirname4 =
|
|
13272
|
-
var pkgPath =
|
|
13273
|
-
var pkg2 = JSON.parse(
|
|
13274
|
-
var
|
|
13632
|
+
var __dirname4 = dirname11(fileURLToPath6(import.meta.url));
|
|
13633
|
+
var pkgPath = existsSync17(join18(__dirname4, "..", "package.json")) ? join18(__dirname4, "..", "package.json") : join18(__dirname4, "..", "..", "package.json");
|
|
13634
|
+
var pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
13635
|
+
var program3 = new Command;
|
|
13275
13636
|
function resolveScope(options) {
|
|
13276
13637
|
if (options.project)
|
|
13277
13638
|
return "project";
|
|
@@ -13350,7 +13711,7 @@ async function openLocalStoreOrExit(json2) {
|
|
|
13350
13711
|
function readToken(tokenFile) {
|
|
13351
13712
|
if (tokenFile) {
|
|
13352
13713
|
try {
|
|
13353
|
-
return
|
|
13714
|
+
return readFileSync13(tokenFile, "utf-8").trim();
|
|
13354
13715
|
} catch {
|
|
13355
13716
|
return;
|
|
13356
13717
|
}
|
|
@@ -13409,8 +13770,8 @@ function editDistance(a, b) {
|
|
|
13409
13770
|
function suggestHooks(name, max = 3) {
|
|
13410
13771
|
return HOOKS.map((h) => ({ name: h.name, dist: editDistance(name.toLowerCase(), h.name.toLowerCase()) })).filter(({ dist }) => dist <= 4).sort((a, b) => a.dist - b.dist).slice(0, max).map(({ name: n2 }) => n2);
|
|
13411
13772
|
}
|
|
13412
|
-
|
|
13413
|
-
|
|
13773
|
+
program3.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
|
|
13774
|
+
program3.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
13414
13775
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
13415
13776
|
console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
|
|
13416
13777
|
` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
|
|
@@ -13418,7 +13779,7 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
|
|
|
13418
13779
|
}
|
|
13419
13780
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
13420
13781
|
});
|
|
13421
|
-
|
|
13782
|
+
program3.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
|
|
13422
13783
|
if (options.cloudflare) {
|
|
13423
13784
|
if (!options.apiUrl) {
|
|
13424
13785
|
const message = "--cloudflare requires --api-url <url>";
|
|
@@ -13488,13 +13849,13 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
13488
13849
|
if (profile.name) {
|
|
13489
13850
|
console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
|
|
13490
13851
|
}
|
|
13491
|
-
console.log(` ${chalk2.dim("Profile:")} ${
|
|
13852
|
+
console.log(` ${chalk2.dim("Profile:")} ${join18(getProfilesDir(), `${profile.agent_id}.json`)}`);
|
|
13492
13853
|
console.log();
|
|
13493
13854
|
console.log(chalk2.dim(" Install hooks with this profile:"));
|
|
13494
13855
|
console.log(` hooks install gitguard --profile ${profile.agent_id}`);
|
|
13495
13856
|
console.log();
|
|
13496
13857
|
});
|
|
13497
|
-
|
|
13858
|
+
program3.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").option("--enable-mementos", "Enable mementos-context for this invocation (provider assistance remains separately opt-in)").option("--disable-mementos", "Disable mementos-context for this invocation").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
|
|
13498
13859
|
let mementos;
|
|
13499
13860
|
try {
|
|
13500
13861
|
mementos = mementosRegistrationFromFlags(hook, options);
|
|
@@ -13512,11 +13873,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
13512
13873
|
process.exit(1);
|
|
13513
13874
|
}
|
|
13514
13875
|
const hookScript = resolveScriptPath2(hook);
|
|
13515
|
-
if (!hookScript || !
|
|
13876
|
+
if (!hookScript || !existsSync17(hookScript)) {
|
|
13516
13877
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
13517
13878
|
process.exit(1);
|
|
13518
13879
|
}
|
|
13519
|
-
const content =
|
|
13880
|
+
const content = readFileSync13(hookScript);
|
|
13520
13881
|
const sha = sha256Of2(content);
|
|
13521
13882
|
const check = checkScriptHash2(hook, sha);
|
|
13522
13883
|
if (!check.ok) {
|
|
@@ -13622,7 +13983,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
13622
13983
|
process.stderr.write(stderr);
|
|
13623
13984
|
process.exit(exitCode);
|
|
13624
13985
|
});
|
|
13625
|
-
|
|
13986
|
+
program3.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--enable-mementos", "Persistently enable mementos-context (provider assistance remains separately opt-in)").option("--disable-mementos", "Persistently disable mementos-context").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
|
|
13626
13987
|
const scope = resolveScope(options);
|
|
13627
13988
|
const target = resolveTarget(options);
|
|
13628
13989
|
let toInstall = hooks;
|
|
@@ -13866,7 +14227,7 @@ Installing hooks (${scope}, ${target})...
|
|
|
13866
14227
|
console.log(chalk2.dim(`
|
|
13867
14228
|
Registered in ${settingsFile}`));
|
|
13868
14229
|
});
|
|
13869
|
-
|
|
14230
|
+
program3.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
|
|
13870
14231
|
const scope = resolveScope(options);
|
|
13871
14232
|
const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
|
|
13872
14233
|
const storeHooks = listStoreHooks();
|
|
@@ -13975,7 +14336,7 @@ Custom / Registry hooks (${storeHooks.length}, showing ${storeVisible.length}):
|
|
|
13975
14336
|
printDisclosureHint(storeHooks.length - storeVisible.length, "hooks info <name>", { includeAll: true });
|
|
13976
14337
|
}
|
|
13977
14338
|
});
|
|
13978
|
-
|
|
14339
|
+
program3.command("search").argument("<query>", "Search term").option("-n, --limit <n>", "Max rows to show in compact output", "10").option("--verbose", "Show descriptions for search results", false).option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
|
|
13979
14340
|
const results = searchHooks(query);
|
|
13980
14341
|
if (options.json) {
|
|
13981
14342
|
console.log(JSON.stringify(results));
|
|
@@ -13994,7 +14355,7 @@ Found ${results.length} hook(s), showing ${visible.length}:
|
|
|
13994
14355
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
13995
14356
|
printDisclosureHint(results.length - visible.length, "hooks info <name>");
|
|
13996
14357
|
});
|
|
13997
|
-
|
|
14358
|
+
program3.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook (settings registration + store + lock + DB)").action(async (hook, options) => {
|
|
13998
14359
|
const scope = resolveScope(options);
|
|
13999
14360
|
const target = resolveTarget(options);
|
|
14000
14361
|
const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
|
|
@@ -14030,7 +14391,7 @@ program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").opti
|
|
|
14030
14391
|
console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
|
|
14031
14392
|
}
|
|
14032
14393
|
});
|
|
14033
|
-
|
|
14394
|
+
program3.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
|
|
14034
14395
|
if (options.json) {
|
|
14035
14396
|
const result = CATEGORIES.map((cat) => ({
|
|
14036
14397
|
name: cat,
|
|
@@ -14047,7 +14408,7 @@ Categories:
|
|
|
14047
14408
|
console.log(` ${category} (${count})`);
|
|
14048
14409
|
}
|
|
14049
14410
|
});
|
|
14050
|
-
|
|
14411
|
+
program3.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action(async (hook, options) => {
|
|
14051
14412
|
const { resolveHookMeta: resolveHookMeta2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
14052
14413
|
const meta = resolveHookMeta2(hook) ?? getHook(hook);
|
|
14053
14414
|
if (!meta) {
|
|
@@ -14095,7 +14456,7 @@ ${meta.displayName}
|
|
|
14095
14456
|
console.log(chalk2.dim(" \u25CB Not installed in project"));
|
|
14096
14457
|
}
|
|
14097
14458
|
});
|
|
14098
|
-
|
|
14459
|
+
program3.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-f, --fix", "Remove stale registrations (a .bak is written beside each settings file)", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks; --fix removes registrations whose hook cannot resolve").action((options) => {
|
|
14099
14460
|
const scope = resolveScope(options);
|
|
14100
14461
|
const settingsPath = getSettingsPath(scope);
|
|
14101
14462
|
const issues = [];
|
|
@@ -14110,7 +14471,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
14110
14471
|
});
|
|
14111
14472
|
}
|
|
14112
14473
|
const staleNames = new Set(stale.map((finding) => finding.hook));
|
|
14113
|
-
const settingsExist =
|
|
14474
|
+
const settingsExist = existsSync17(settingsPath);
|
|
14114
14475
|
if (!settingsExist) {
|
|
14115
14476
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
14116
14477
|
}
|
|
@@ -14118,7 +14479,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
14118
14479
|
let wiringCount = 0;
|
|
14119
14480
|
if (settingsExist) {
|
|
14120
14481
|
try {
|
|
14121
|
-
const settings = JSON.parse(
|
|
14482
|
+
const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
|
|
14122
14483
|
wiringCount = countSettingsWiring(settings);
|
|
14123
14484
|
} catch {}
|
|
14124
14485
|
}
|
|
@@ -14129,7 +14490,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
14129
14490
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
14130
14491
|
let hookHealthy = true;
|
|
14131
14492
|
if (custom) {
|
|
14132
|
-
if (!
|
|
14493
|
+
if (!existsSync17(custom.scriptPath)) {
|
|
14133
14494
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
14134
14495
|
hookHealthy = false;
|
|
14135
14496
|
}
|
|
@@ -14140,15 +14501,15 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
14140
14501
|
continue;
|
|
14141
14502
|
}
|
|
14142
14503
|
const hookDir = getHookPath(name);
|
|
14143
|
-
const hookScript =
|
|
14144
|
-
if (!
|
|
14504
|
+
const hookScript = join18(hookDir, "src", "hook.ts");
|
|
14505
|
+
if (!existsSync17(hookScript)) {
|
|
14145
14506
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
14146
14507
|
hookHealthy = false;
|
|
14147
14508
|
}
|
|
14148
14509
|
}
|
|
14149
14510
|
if (meta && settingsExist) {
|
|
14150
14511
|
try {
|
|
14151
|
-
const settings = JSON.parse(
|
|
14512
|
+
const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
|
|
14152
14513
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
14153
14514
|
const eventName = meta.event.split(":")[0];
|
|
14154
14515
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -14235,7 +14596,7 @@ Hook Health Check (${scope})
|
|
|
14235
14596
|
}
|
|
14236
14597
|
console.log();
|
|
14237
14598
|
});
|
|
14238
|
-
|
|
14599
|
+
program3.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version), refresh lock pins, or install a pinned registry version (<name>@<version>)").action(async (hooks, options) => {
|
|
14239
14600
|
const scope = resolveScope(options);
|
|
14240
14601
|
const installed = getInstalledHooks(scope);
|
|
14241
14602
|
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
@@ -14338,7 +14699,7 @@ Updating hooks...
|
|
|
14338
14699
|
process.exitCode = 1;
|
|
14339
14700
|
}
|
|
14340
14701
|
});
|
|
14341
|
-
|
|
14702
|
+
program3.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("--verbose", "Print full hook README content", false).option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
|
|
14342
14703
|
if (hook) {
|
|
14343
14704
|
const meta = getHook(hook);
|
|
14344
14705
|
if (!meta) {
|
|
@@ -14351,10 +14712,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
14351
14712
|
return;
|
|
14352
14713
|
}
|
|
14353
14714
|
const hookPath = getHookPath(hook);
|
|
14354
|
-
const readmePath =
|
|
14715
|
+
const readmePath = join18(hookPath, "README.md");
|
|
14355
14716
|
let readme = "";
|
|
14356
|
-
if (
|
|
14357
|
-
readme =
|
|
14717
|
+
if (existsSync17(readmePath)) {
|
|
14718
|
+
readme = readFileSync13(readmePath, "utf-8");
|
|
14358
14719
|
}
|
|
14359
14720
|
if (options.json) {
|
|
14360
14721
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -14472,7 +14833,7 @@ ${meta.displayName} v${meta.version}
|
|
|
14472
14833
|
console.log(` hooks docs --json Machine-readable documentation`);
|
|
14473
14834
|
console.log();
|
|
14474
14835
|
});
|
|
14475
|
-
|
|
14836
|
+
program3.command("upgrade").option("-c, --check", "Check for updates without installing", false).option("-j, --json", "Output as JSON", false).description("Update the @hasna/hooks package to the latest version").action(async (options) => {
|
|
14476
14837
|
const current2 = pkg2.version;
|
|
14477
14838
|
let pm = "npm";
|
|
14478
14839
|
try {
|
|
@@ -14534,7 +14895,7 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
14534
14895
|
\u2713 Upgraded: ${current2} \u2192 ${latest}`));
|
|
14535
14896
|
}
|
|
14536
14897
|
});
|
|
14537
|
-
|
|
14898
|
+
program3.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
|
|
14538
14899
|
const profiles = exportProfiles();
|
|
14539
14900
|
const json2 = JSON.stringify(profiles, null, 2);
|
|
14540
14901
|
if (options.output) {
|
|
@@ -14546,14 +14907,14 @@ program2.command("profile-export").description("Export all agent profiles as JSO
|
|
|
14546
14907
|
console.log(json2);
|
|
14547
14908
|
}
|
|
14548
14909
|
});
|
|
14549
|
-
|
|
14910
|
+
program3.command("profile-import").argument("<file>", "JSON file to import profiles from (use - for stdin)").description("Import agent profiles from a JSON export file").option("-j, --json", "Output result as JSON", false).action(async (file, options) => {
|
|
14550
14911
|
let raw;
|
|
14551
14912
|
if (file === "-") {
|
|
14552
14913
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
14553
14914
|
} else {
|
|
14554
|
-
const { readFileSync:
|
|
14915
|
+
const { readFileSync: readFileSync14 } = await import("fs");
|
|
14555
14916
|
try {
|
|
14556
|
-
raw =
|
|
14917
|
+
raw = readFileSync14(file, "utf-8");
|
|
14557
14918
|
} catch {
|
|
14558
14919
|
if (options.json) {
|
|
14559
14920
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -14584,7 +14945,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
14584
14945
|
console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
|
|
14585
14946
|
}
|
|
14586
14947
|
});
|
|
14587
|
-
var logCmd =
|
|
14948
|
+
var logCmd = program3.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
|
|
14588
14949
|
logCmd.command("list").description("List hook events").option("--hook <name>", "Filter by hook name").option("--session <id>", "Filter by session ID").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14589
14950
|
const db = await openLocalStoreOrExit(options.json);
|
|
14590
14951
|
const limit = parseInt(options.limit) || 50;
|
|
@@ -14735,7 +15096,7 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
|
|
|
14735
15096
|
}
|
|
14736
15097
|
console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
|
|
14737
15098
|
});
|
|
14738
|
-
var storageCmd =
|
|
15099
|
+
var storageCmd = program3.command("storage").description("Sync local hook data with storage PostgreSQL");
|
|
14739
15100
|
storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14740
15101
|
try {
|
|
14741
15102
|
const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
@@ -14818,7 +15179,7 @@ storageCmd.command("sync").description("Bidirectional storage sync: pull then pu
|
|
|
14818
15179
|
process.exitCode = 1;
|
|
14819
15180
|
}
|
|
14820
15181
|
});
|
|
14821
|
-
|
|
15182
|
+
program3.command("trust").argument("<hook>", "Hook to trust").option("-j, --json", "Output as JSON", false).description("Trust the current script content of a hook (re-pins its sha256)").action(async (hook, options) => {
|
|
14822
15183
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
14823
15184
|
const { retrustHook: retrustHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
14824
15185
|
const resolved = resolveHook2(hook);
|
|
@@ -14837,14 +15198,14 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
|
|
|
14837
15198
|
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
14838
15199
|
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
14839
15200
|
});
|
|
14840
|
-
|
|
15201
|
+
program3.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from the @hasna/contracts chain per request (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials) \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
|
|
14841
15202
|
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
14842
15203
|
startServeServer2({
|
|
14843
15204
|
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
14844
15205
|
host: options.host
|
|
14845
15206
|
});
|
|
14846
15207
|
});
|
|
14847
|
-
|
|
15208
|
+
program3.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no registry credential resolves and local mode is opted into)").action(async (options) => {
|
|
14848
15209
|
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
14849
15210
|
try {
|
|
14850
15211
|
const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
|
|
@@ -14888,7 +15249,7 @@ Hook sync (${source})${plan.dryRun ? " \u2014 dry run" : ""}
|
|
|
14888
15249
|
process.exitCode = 1;
|
|
14889
15250
|
}
|
|
14890
15251
|
});
|
|
14891
|
-
|
|
15252
|
+
program3.command("cf").description("Provision and deploy the hooks registry to Cloudflare").option("--deploy", "Provision D1 + R2 via the Cloudflare API", true).option("--dry-run", "Print the plan without calling the Cloudflare API", false).option("--account-id <id>", "Cloudflare account id (defaults to CF_ACCOUNT_ID env)").option("--database-name <name>", "D1 database name (default hooks-registry)").option("--bucket-name <name>", "R2 bucket name (default hooks-registry-artifacts)").option("--token-file <path>", "Read the API token from a file (defaults to CF_API_TOKEN env)").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14892
15253
|
const token = readToken(options.tokenFile);
|
|
14893
15254
|
const accountId = options.accountId ?? process.env.CF_ACCOUNT_ID;
|
|
14894
15255
|
if (!options.dryRun) {
|
|
@@ -14937,7 +15298,7 @@ Cloudflare provisioning complete:
|
|
|
14937
15298
|
console.log(chalk2.dim(" wrangler.toml, fill in the D1 database id, then run the commands above."));
|
|
14938
15299
|
console.log();
|
|
14939
15300
|
});
|
|
14940
|
-
|
|
15301
|
+
program3.command("migrate").description("Apply PostgreSQL migrations to the storage database (HASNA_HOOKS_DATABASE_URL)").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14941
15302
|
try {
|
|
14942
15303
|
const { getStoragePg: getStoragePg2, runStorageMigrations: runStorageMigrations2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14943
15304
|
const remote = await getStoragePg2();
|
|
@@ -14960,7 +15321,7 @@ program2.command("migrate").description("Apply PostgreSQL migrations to the stor
|
|
|
14960
15321
|
process.exitCode = 1;
|
|
14961
15322
|
}
|
|
14962
15323
|
});
|
|
14963
|
-
|
|
15324
|
+
program3.command("mcp").option("-s, --stdio", "Use stdio transport (one process per agent)", false).option("--sse", "Use legacy SSE transport (port 39427)", false).option("--http", "Use Streamable HTTP transport (explicit; this is also the default)", false).option("-p, --port <port>", "Port for HTTP/SSE transport (defaults to 8847 for HTTP, 39427 for SSE)").description("Start MCP server for AI agent integration (default: shared Streamable HTTP)").action(async (options) => {
|
|
14964
15325
|
if (options.stdio) {
|
|
14965
15326
|
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14966
15327
|
await startStdioServer2();
|
|
@@ -14981,8 +15342,20 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14981
15342
|
startMcpHttpServer2({ name: "hooks", port: resolveMcpHttpPort2(args), buildServer: createHooksServer2 });
|
|
14982
15343
|
}
|
|
14983
15344
|
});
|
|
14984
|
-
registerEventsCommands(
|
|
15345
|
+
registerEventsCommands(program3, { source: "hooks" });
|
|
15346
|
+
program3.command("safety").description("Register bundled native deletion guards without a registry credential").command("install <guard>").option("--target <target>", "codex or claude", "codex").option("--scope <scope>", "global or project", "global").option("--overwrite", "Replace this guard in place; native trust is still required").action((guard, options) => {
|
|
15347
|
+
if (!isNativeSafetyName(guard) || !["codex", "claude"].includes(options.target) || !["global", "project"].includes(options.scope)) {
|
|
15348
|
+
console.error("Unsupported bundled safety registration");
|
|
15349
|
+
process.exitCode = 1;
|
|
15350
|
+
return;
|
|
15351
|
+
}
|
|
15352
|
+
const result = installHook(guard, { target: options.target, scope: options.scope, overwrite: options.overwrite });
|
|
15353
|
+
console.log(JSON.stringify(result));
|
|
15354
|
+
if (!result.success)
|
|
15355
|
+
process.exitCode = 1;
|
|
15356
|
+
});
|
|
14985
15357
|
var API_INDEPENDENT_COMMANDS = new Set([
|
|
15358
|
+
"safety",
|
|
14986
15359
|
"serve",
|
|
14987
15360
|
"mcp",
|
|
14988
15361
|
"cf",
|
|
@@ -15043,4 +15416,4 @@ function enforceTransportGate() {
|
|
|
15043
15416
|
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
|
|
15044
15417
|
}
|
|
15045
15418
|
enforceTransportGate();
|
|
15046
|
-
|
|
15419
|
+
program3.parse();
|