@hasna/hooks 0.8.0 → 0.9.1
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 +33 -5
- package/bin/hooks-mcp.js +4862 -0
- package/bin/index.js +592 -170
- package/bin/serve.js +33 -10
- package/dist/db/index.d.ts +8 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2870 -2709
- package/dist/lib/db-writer.d.ts +30 -1
- package/dist/lib/installer.d.ts +40 -1
- package/dist/lib/local-opt-in.d.ts +26 -0
- package/dist/lib/registration.d.ts +75 -0
- package/dist/lib/registry.d.ts +14 -0
- package/dist/lib/sync.d.ts +17 -1
- package/dist/sdk/index.d.ts +103 -0
- package/dist/sdk/index.js +1050 -0
- package/dist/storage.js +15 -1
- package/hooks/hook-agent-rules-version-check/README.md +1 -1
- package/hooks/hook-trash-guard/README.md +147 -0
- package/hooks/hook-trash-guard/package.json +12 -0
- package/hooks/hook-trash-guard/src/hook.ts +1142 -0
- package/package.json +10 -3
- package/scripts/validate-package.ts +31 -4
- package/hooks/codewith-native-common.test.ts +0 -1935
- package/hooks/hook-affected-tests/tsconfig.json +0 -25
- package/hooks/hook-agent-rules-version-check/src/hook.test.ts +0 -104
- package/hooks/hook-agent-rules-version-check/tsconfig.json +0 -25
- package/hooks/hook-announce-start/tsconfig.json +0 -25
- package/hooks/hook-announce-stop/tsconfig.json +0 -25
- package/hooks/hook-autoformat/tsconfig.json +0 -25
- package/hooks/hook-branchprotect/tsconfig.json +0 -25
- package/hooks/hook-checkbugs/tsconfig.json +0 -15
- package/hooks/hook-checkdocs/tsconfig.json +0 -15
- package/hooks/hook-checkfiles/tsconfig.json +0 -15
- package/hooks/hook-checklint/tsconfig.json +0 -15
- package/hooks/hook-checkpoint/tsconfig.json +0 -25
- package/hooks/hook-checksecurity/tsconfig.json +0 -15
- package/hooks/hook-checktasks/tsconfig.json +0 -20
- package/hooks/hook-checktests/tsconfig.json +0 -15
- package/hooks/hook-conflict-detect/tsconfig.json +0 -25
- package/hooks/hook-contextrefresh/tsconfig.json +0 -25
- package/hooks/hook-desktopnotify/tsconfig.json +0 -25
- package/hooks/hook-dm-inject/tsconfig.json +0 -25
- package/hooks/hook-envsetup/tsconfig.json +0 -25
- package/hooks/hook-failure-to-task/tsconfig.json +0 -25
- package/hooks/hook-filelock/tsconfig.json +0 -25
- package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +0 -302
- package/hooks/hook-fleet-blockers-gate/tsconfig.json +0 -25
- package/hooks/hook-fleet-catchup/src/hook.test.ts +0 -156
- package/hooks/hook-fleet-catchup/tsconfig.json +0 -25
- package/hooks/hook-gitguard/tsconfig.json +0 -25
- package/hooks/hook-knowledge-context/src/hook.test.ts +0 -379
- package/hooks/hook-packageage/tsconfig.json +0 -25
- package/hooks/hook-permissionguard/tsconfig.json +0 -25
- package/hooks/hook-phonenotify/tsconfig.json +0 -25
- package/hooks/hook-precompact/tsconfig.json +0 -25
- package/hooks/hook-protectfiles/tsconfig.json +0 -25
- package/hooks/hook-scanoutput/src/hook.test.ts +0 -217
- package/hooks/hook-spiral-detector/src/hook.test.ts +0 -72
- package/hooks/hook-stylescheck/tsconfig.json +0 -25
- package/hooks/hook-typecheck-gate/tsconfig.json +0 -25
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +0 -466
- package/hooks/hook-workspace-repos-guard/tsconfig.json +0 -21
- package/hooks/mention-context/src/hook.test.ts +0 -68
package/bin/index.js
CHANGED
|
@@ -2083,6 +2083,18 @@ var init_registry = __esm(() => {
|
|
|
2083
2083
|
matcher: "^(Bash|Write|Edit|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
2084
2084
|
tags: ["workspace", "repos", "structure", "guard", "safety", "orgs", "multi-agent"]
|
|
2085
2085
|
},
|
|
2086
|
+
{
|
|
2087
|
+
name: "trash-guard",
|
|
2088
|
+
displayName: "Trash Guard",
|
|
2089
|
+
description: "Rewrites rm issued through Bash into `trash guard`, so the delete lands in a recoverable trash store; refuses the delete when there is nothing to redirect to (trash absent, a delete verb it cannot rewrite, or the protected class: /, ~, ~/.hasna, ~/.ssh, ~/.aws)",
|
|
2090
|
+
version: "0.1.1",
|
|
2091
|
+
category: "Git Safety",
|
|
2092
|
+
event: "PreToolUse",
|
|
2093
|
+
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
2094
|
+
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
2095
|
+
rewritesInput: true,
|
|
2096
|
+
timeoutSeconds: 5
|
|
2097
|
+
},
|
|
2086
2098
|
{
|
|
2087
2099
|
name: "checktests",
|
|
2088
2100
|
displayName: "Check Tests",
|
|
@@ -4477,6 +4489,111 @@ var init_resolve = __esm(() => {
|
|
|
4477
4489
|
init_manifest();
|
|
4478
4490
|
});
|
|
4479
4491
|
|
|
4492
|
+
// src/lib/registration.ts
|
|
4493
|
+
function registrationName(command) {
|
|
4494
|
+
if (typeof command !== "string")
|
|
4495
|
+
return;
|
|
4496
|
+
const match = command.trim().match(REGISTRATION_COMMAND_RE);
|
|
4497
|
+
return match ? match[1] ?? match[2] : undefined;
|
|
4498
|
+
}
|
|
4499
|
+
function findStaleRegistrations(settings, file, resolves = (name) => resolveHook(name) !== undefined) {
|
|
4500
|
+
const hooks = settings.hooks;
|
|
4501
|
+
if (!hooks || typeof hooks !== "object")
|
|
4502
|
+
return [];
|
|
4503
|
+
const stale = [];
|
|
4504
|
+
for (const eventKey of Object.keys(hooks)) {
|
|
4505
|
+
const entries = hooks[eventKey];
|
|
4506
|
+
if (!Array.isArray(entries))
|
|
4507
|
+
continue;
|
|
4508
|
+
for (const entry of entries) {
|
|
4509
|
+
if (!Array.isArray(entry?.hooks))
|
|
4510
|
+
continue;
|
|
4511
|
+
for (const hook of entry.hooks) {
|
|
4512
|
+
const name = registrationName(hook?.command);
|
|
4513
|
+
if (name === undefined || resolves(name))
|
|
4514
|
+
continue;
|
|
4515
|
+
stale.push({ file, event: eventKey, hook: name, command: hook.command });
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
return stale;
|
|
4520
|
+
}
|
|
4521
|
+
function matchersOverlap(a, b) {
|
|
4522
|
+
return a === b || a.includes(b) || b.includes(a);
|
|
4523
|
+
}
|
|
4524
|
+
function hookEventsOf(info) {
|
|
4525
|
+
return info.events?.length ? info.events : info.event ? [info.event] : [];
|
|
4526
|
+
}
|
|
4527
|
+
function findRewriteOverlaps(names, lookup) {
|
|
4528
|
+
const rewriting = names.map((name) => ({ name, info: lookup(name) })).filter((row) => Boolean(row.info?.rewritesInput && row.info.matcher));
|
|
4529
|
+
const overlaps = [];
|
|
4530
|
+
for (let i = 0;i < rewriting.length; i++) {
|
|
4531
|
+
for (let j = i + 1;j < rewriting.length; j++) {
|
|
4532
|
+
const a = rewriting[i];
|
|
4533
|
+
const b = rewriting[j];
|
|
4534
|
+
const aEvents = hookEventsOf(a.info);
|
|
4535
|
+
const bEvents = hookEventsOf(b.info);
|
|
4536
|
+
const event = aEvents.find((name) => name === "PreToolUse" && bEvents.includes(name));
|
|
4537
|
+
if (!event)
|
|
4538
|
+
continue;
|
|
4539
|
+
const aMatcher = a.info.matcher;
|
|
4540
|
+
const bMatcher = b.info.matcher;
|
|
4541
|
+
if (!matchersOverlap(aMatcher.toLowerCase(), bMatcher.toLowerCase()))
|
|
4542
|
+
continue;
|
|
4543
|
+
overlaps.push({ hooks: [a.name, b.name], event, matchers: [aMatcher, bMatcher] });
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
return overlaps;
|
|
4547
|
+
}
|
|
4548
|
+
function safeMatcherTest(pattern, value) {
|
|
4549
|
+
try {
|
|
4550
|
+
return new RegExp(pattern).test(value);
|
|
4551
|
+
} catch {
|
|
4552
|
+
return false;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
function countSettingsWiring(settings) {
|
|
4556
|
+
const hooks = settings.hooks;
|
|
4557
|
+
if (!hooks || typeof hooks !== "object")
|
|
4558
|
+
return 0;
|
|
4559
|
+
let count = 0;
|
|
4560
|
+
for (const eventKey of Object.keys(hooks)) {
|
|
4561
|
+
const entries = hooks[eventKey];
|
|
4562
|
+
if (!Array.isArray(entries))
|
|
4563
|
+
continue;
|
|
4564
|
+
for (const entry of entries) {
|
|
4565
|
+
if (Array.isArray(entry?.hooks))
|
|
4566
|
+
count += entry.hooks.length;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
return count;
|
|
4570
|
+
}
|
|
4571
|
+
function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
4572
|
+
const separator = eventSpec.indexOf(":");
|
|
4573
|
+
const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
|
|
4574
|
+
const specMatcher = separator === -1 ? "" : eventSpec.slice(separator + 1);
|
|
4575
|
+
const matcher = specMatcher || hookMatcher || "";
|
|
4576
|
+
const hooks = settings.hooks?.[eventName];
|
|
4577
|
+
if (!Array.isArray(hooks))
|
|
4578
|
+
return false;
|
|
4579
|
+
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
4580
|
+
const match = h?.command?.match(/^hooks run ([\w-]+)/);
|
|
4581
|
+
if (!match || match[1] !== name)
|
|
4582
|
+
return false;
|
|
4583
|
+
if (!matcher)
|
|
4584
|
+
return true;
|
|
4585
|
+
const entryMatcher = typeof entry.matcher === "string" ? entry.matcher : "";
|
|
4586
|
+
if (!entryMatcher)
|
|
4587
|
+
return true;
|
|
4588
|
+
return entryMatcher === matcher || safeMatcherTest(entryMatcher, matcher) || safeMatcherTest(matcher, entryMatcher);
|
|
4589
|
+
}));
|
|
4590
|
+
}
|
|
4591
|
+
var REGISTRATION_COMMAND_RE;
|
|
4592
|
+
var init_registration = __esm(() => {
|
|
4593
|
+
init_resolve();
|
|
4594
|
+
REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|hook-([\w-]+))$/;
|
|
4595
|
+
});
|
|
4596
|
+
|
|
4480
4597
|
// src/db/schema.ts
|
|
4481
4598
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
4482
4599
|
CREATE TABLE IF NOT EXISTS hook_events (
|
|
@@ -4802,14 +4919,30 @@ function runRetention(db, days) {
|
|
|
4802
4919
|
// src/db/index.ts
|
|
4803
4920
|
var exports_db = {};
|
|
4804
4921
|
__export(exports_db, {
|
|
4922
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
4923
|
+
localStoreRefusalMessage: () => localStoreRefusalMessage,
|
|
4924
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
4805
4925
|
getDbPath: () => getDbPath,
|
|
4806
4926
|
getDb: () => getDb,
|
|
4807
4927
|
createTestDb: () => createTestDb,
|
|
4808
|
-
closeDb: () => closeDb
|
|
4928
|
+
closeDb: () => closeDb,
|
|
4929
|
+
allowLocalStore: () => allowLocalStore
|
|
4809
4930
|
});
|
|
4810
4931
|
import { Database } from "bun:sqlite";
|
|
4811
4932
|
import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
4812
4933
|
import { join as join9 } from "path";
|
|
4934
|
+
function refuseLocalStore(message) {
|
|
4935
|
+
localStoreRefusal = message;
|
|
4936
|
+
}
|
|
4937
|
+
function allowLocalStore() {
|
|
4938
|
+
localStoreRefusal = null;
|
|
4939
|
+
}
|
|
4940
|
+
function isLocalStoreRefused() {
|
|
4941
|
+
return localStoreRefusal !== null;
|
|
4942
|
+
}
|
|
4943
|
+
function localStoreRefusalMessage() {
|
|
4944
|
+
return localStoreRefusal;
|
|
4945
|
+
}
|
|
4813
4946
|
function resolveDataDir() {
|
|
4814
4947
|
const effective = getEffectiveDataRoot();
|
|
4815
4948
|
const oldDir = join9(getHomeDir(), ".hooks");
|
|
@@ -4833,6 +4966,8 @@ function ensureDir(dbPath) {
|
|
|
4833
4966
|
}
|
|
4834
4967
|
}
|
|
4835
4968
|
function getDb() {
|
|
4969
|
+
if (localStoreRefusal !== null)
|
|
4970
|
+
throw new Error(localStoreRefusal);
|
|
4836
4971
|
if (instance)
|
|
4837
4972
|
return instance;
|
|
4838
4973
|
const dbPath = getDbPath();
|
|
@@ -4870,7 +5005,7 @@ function createTestDb() {
|
|
|
4870
5005
|
db.exec("PRAGMA foreign_keys=ON");
|
|
4871
5006
|
return db;
|
|
4872
5007
|
}
|
|
4873
|
-
var instance = null;
|
|
5008
|
+
var instance = null, localStoreRefusal = null;
|
|
4874
5009
|
var init_db = __esm(() => {
|
|
4875
5010
|
init_app_home();
|
|
4876
5011
|
init_migrations();
|
|
@@ -4909,6 +5044,9 @@ async function sha256File(path) {
|
|
|
4909
5044
|
const buf = await readFile2(path);
|
|
4910
5045
|
return sha256Of(buf);
|
|
4911
5046
|
}
|
|
5047
|
+
function storeDb() {
|
|
5048
|
+
return isLocalStoreRefused() ? null : getDb();
|
|
5049
|
+
}
|
|
4912
5050
|
function getHookRecord(db, name) {
|
|
4913
5051
|
return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
|
|
4914
5052
|
}
|
|
@@ -5009,26 +5147,28 @@ function removePinnedHook(name) {
|
|
|
5009
5147
|
}
|
|
5010
5148
|
function pinInstalledHook(name, version, sha256, source, sourceRef) {
|
|
5011
5149
|
const now2 = new Date().toISOString();
|
|
5012
|
-
const db =
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5150
|
+
const db = storeDb();
|
|
5151
|
+
if (db) {
|
|
5152
|
+
upsertHookRecord(db, {
|
|
5153
|
+
name,
|
|
5154
|
+
version,
|
|
5155
|
+
sha256,
|
|
5156
|
+
source_type: source,
|
|
5157
|
+
source_ref: sourceRef ?? null,
|
|
5158
|
+
last_verified_at: now2
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5021
5161
|
setPinnedHook(name, { version, sha256, source });
|
|
5022
5162
|
}
|
|
5023
5163
|
function removeHookFromStore(name) {
|
|
5024
5164
|
const removedPin = removePinnedHook(name);
|
|
5025
|
-
const db =
|
|
5026
|
-
const removedRecord = removeHookRecord(db, name);
|
|
5165
|
+
const db = storeDb();
|
|
5166
|
+
const removedRecord = db ? removeHookRecord(db, name) : false;
|
|
5027
5167
|
return { removedPin, removedRecord };
|
|
5028
5168
|
}
|
|
5029
5169
|
function checkScriptHash(name, actual) {
|
|
5030
|
-
const db =
|
|
5031
|
-
const record = getHookRecord(db, name);
|
|
5170
|
+
const db = storeDb();
|
|
5171
|
+
const record = db ? getHookRecord(db, name) : null;
|
|
5032
5172
|
const pin = getPinnedHook(name);
|
|
5033
5173
|
const expected = record?.sha256 ?? pin?.sha256 ?? null;
|
|
5034
5174
|
if (expected !== null && expected !== actual) {
|
|
@@ -5038,24 +5178,28 @@ function checkScriptHash(name, actual) {
|
|
|
5038
5178
|
const now3 = new Date().toISOString();
|
|
5039
5179
|
const version = pin?.version ?? record?.version ?? "0.0.0";
|
|
5040
5180
|
const source = pin?.source ?? record?.source_type ?? "local";
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5181
|
+
if (db) {
|
|
5182
|
+
upsertHookRecord(db, {
|
|
5183
|
+
name,
|
|
5184
|
+
version,
|
|
5185
|
+
sha256: actual,
|
|
5186
|
+
source_type: source,
|
|
5187
|
+
last_verified_at: now3
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5048
5190
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
5049
5191
|
return { ok: true, pinned: false, expected: null, actual, name };
|
|
5050
5192
|
}
|
|
5051
5193
|
const now2 = new Date().toISOString();
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5194
|
+
if (db) {
|
|
5195
|
+
upsertHookRecord(db, {
|
|
5196
|
+
name,
|
|
5197
|
+
version: record?.version ?? pin?.version ?? "0.0.0",
|
|
5198
|
+
sha256: expected,
|
|
5199
|
+
source_type: record?.source_type ?? pin?.source ?? "local",
|
|
5200
|
+
last_verified_at: now2
|
|
5201
|
+
});
|
|
5202
|
+
}
|
|
5059
5203
|
return { ok: true, pinned: true, expected, actual, name };
|
|
5060
5204
|
}
|
|
5061
5205
|
async function verifyScriptHash(name, scriptPath) {
|
|
@@ -5063,14 +5207,16 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5063
5207
|
}
|
|
5064
5208
|
function retrustHook(name, scriptPath, version, source) {
|
|
5065
5209
|
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5066
|
-
const db =
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5210
|
+
const db = storeDb();
|
|
5211
|
+
if (db) {
|
|
5212
|
+
upsertHookRecord(db, {
|
|
5213
|
+
name,
|
|
5214
|
+
version,
|
|
5215
|
+
sha256: actual,
|
|
5216
|
+
source_type: source,
|
|
5217
|
+
last_verified_at: new Date().toISOString()
|
|
5218
|
+
});
|
|
5219
|
+
}
|
|
5074
5220
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
5075
5221
|
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
5076
5222
|
}
|
|
@@ -5090,8 +5236,10 @@ var init_store = __esm(() => {
|
|
|
5090
5236
|
var exports_installer = {};
|
|
5091
5237
|
__export(exports_installer, {
|
|
5092
5238
|
uninstallHook: () => uninstallHook,
|
|
5239
|
+
scanStaleRegistrations: () => scanStaleRegistrations,
|
|
5093
5240
|
removeHook: () => removeHook,
|
|
5094
5241
|
removeCodewithHookEntry: () => removeCodewithHookEntry,
|
|
5242
|
+
pruneStaleRegistrations: () => pruneStaleRegistrations,
|
|
5095
5243
|
isEventSupported: () => isEventSupported,
|
|
5096
5244
|
installHooks: () => installHooks,
|
|
5097
5245
|
installHook: () => installHook,
|
|
@@ -5101,6 +5249,7 @@ __export(exports_installer, {
|
|
|
5101
5249
|
getRegisteredHooks: () => getRegisteredHooks,
|
|
5102
5250
|
getInstalledHooks: () => getInstalledHooks,
|
|
5103
5251
|
getHookPath: () => getHookPath,
|
|
5252
|
+
detectRewriteConflict: () => detectRewriteConflict,
|
|
5104
5253
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5105
5254
|
});
|
|
5106
5255
|
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -5207,6 +5356,7 @@ function codewithTimeout(name) {
|
|
|
5207
5356
|
case "prompt-guard":
|
|
5208
5357
|
return 3;
|
|
5209
5358
|
case "worktree-guard":
|
|
5359
|
+
case "trash-guard":
|
|
5210
5360
|
case "stop-sync":
|
|
5211
5361
|
return 5;
|
|
5212
5362
|
default:
|
|
@@ -5225,6 +5375,8 @@ function codewithStatusMessage(name) {
|
|
|
5225
5375
|
return "Checking prompt safety";
|
|
5226
5376
|
case "worktree-guard":
|
|
5227
5377
|
return "Checking worktree safety";
|
|
5378
|
+
case "trash-guard":
|
|
5379
|
+
return "Redirecting rm into trash";
|
|
5228
5380
|
case "stop-sync":
|
|
5229
5381
|
return "Syncing turn-end heartbeat";
|
|
5230
5382
|
default:
|
|
@@ -5292,14 +5444,32 @@ function detectConflict(name, scope, target) {
|
|
|
5292
5444
|
continue;
|
|
5293
5445
|
if (!getHookEvents(existing).some((event) => events.has(event)))
|
|
5294
5446
|
continue;
|
|
5295
|
-
|
|
5296
|
-
const b = existing.matcher.toLowerCase();
|
|
5297
|
-
if (a === b || a.includes(b) || b.includes(a)) {
|
|
5447
|
+
if (matchersOverlap(meta.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
|
|
5298
5448
|
return `conflicts with '${existingName}' (same event ${meta.event}, overlapping matcher '${existing.matcher}')`;
|
|
5299
5449
|
}
|
|
5300
5450
|
}
|
|
5301
5451
|
return;
|
|
5302
5452
|
}
|
|
5453
|
+
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
5454
|
+
const meta = resolveHookMeta(name);
|
|
5455
|
+
if (!meta?.matcher || !meta.rewritesInput)
|
|
5456
|
+
return;
|
|
5457
|
+
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
5458
|
+
return;
|
|
5459
|
+
for (const existingName of getRegisteredHooksForTarget(scope, target)) {
|
|
5460
|
+
if (existingName === name)
|
|
5461
|
+
continue;
|
|
5462
|
+
const existing = lookup(existingName);
|
|
5463
|
+
if (!existing?.matcher || !existing.rewritesInput)
|
|
5464
|
+
continue;
|
|
5465
|
+
if (!getHookEvents(existing).includes("PreToolUse"))
|
|
5466
|
+
continue;
|
|
5467
|
+
if (!matchersOverlap(meta.matcher.toLowerCase(), existing.matcher.toLowerCase()))
|
|
5468
|
+
continue;
|
|
5469
|
+
return `both '${name}' and '${existingName}' rewrite the tool input on overlapping PreToolUse matchers ('${meta.matcher}' / '${existing.matcher}'). Only one rewrite per tool call is applied, so one guard would silently disappear \u2014 install one of them, or narrow a matcher so they no longer overlap.`;
|
|
5470
|
+
}
|
|
5471
|
+
return;
|
|
5472
|
+
}
|
|
5303
5473
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
|
|
5304
5474
|
const shortName = shortHookName(name);
|
|
5305
5475
|
if (!hookExists(shortName)) {
|
|
@@ -5359,6 +5529,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
5359
5529
|
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
5360
5530
|
}
|
|
5361
5531
|
const conflict = detectConflict(shortName, scope, target);
|
|
5532
|
+
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
5533
|
+
if (rewriteConflict) {
|
|
5534
|
+
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
5535
|
+
}
|
|
5362
5536
|
try {
|
|
5363
5537
|
registerHook(shortName, scope, target, profile);
|
|
5364
5538
|
return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
|
|
@@ -5430,8 +5604,12 @@ function registerHook(name, scope = "global", target = "claude", profile) {
|
|
|
5430
5604
|
for (const eventKey of uniqueEventKeys) {
|
|
5431
5605
|
if (!settings.hooks[eventKey])
|
|
5432
5606
|
settings.hooks[eventKey] = [];
|
|
5607
|
+
const hookEntry = { type: "command", command: hookCommand };
|
|
5608
|
+
if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0) {
|
|
5609
|
+
hookEntry.timeout = meta.timeoutSeconds;
|
|
5610
|
+
}
|
|
5433
5611
|
const entry = {
|
|
5434
|
-
hooks: [
|
|
5612
|
+
hooks: [hookEntry]
|
|
5435
5613
|
};
|
|
5436
5614
|
if (meta.matcher) {
|
|
5437
5615
|
entry.matcher = meta.matcher;
|
|
@@ -5515,6 +5693,74 @@ function removeHookForTarget(name, scope, target) {
|
|
|
5515
5693
|
unregisterHook(name, scope, target);
|
|
5516
5694
|
return true;
|
|
5517
5695
|
}
|
|
5696
|
+
function readSettingsAt(path) {
|
|
5697
|
+
try {
|
|
5698
|
+
if (!existsSync10(path))
|
|
5699
|
+
return;
|
|
5700
|
+
const raw = readFileSync4(path, "utf-8");
|
|
5701
|
+
return { raw, settings: JSON.parse(raw) };
|
|
5702
|
+
} catch {
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
function scanStaleRegistrations(scope = "global") {
|
|
5707
|
+
const stale = [];
|
|
5708
|
+
for (const target of JSON_SETTINGS_TARGETS) {
|
|
5709
|
+
const path = getSettingsPath(scope, target);
|
|
5710
|
+
const parsed = readSettingsAt(path);
|
|
5711
|
+
if (!parsed)
|
|
5712
|
+
continue;
|
|
5713
|
+
stale.push(...findStaleRegistrations(parsed.settings, path));
|
|
5714
|
+
}
|
|
5715
|
+
return stale;
|
|
5716
|
+
}
|
|
5717
|
+
function pruneStaleRegistrations(scope = "global") {
|
|
5718
|
+
const results = [];
|
|
5719
|
+
for (const target of JSON_SETTINGS_TARGETS) {
|
|
5720
|
+
const path = getSettingsPath(scope, target);
|
|
5721
|
+
const parsed = readSettingsAt(path);
|
|
5722
|
+
if (!parsed)
|
|
5723
|
+
continue;
|
|
5724
|
+
const stale = findStaleRegistrations(parsed.settings, path);
|
|
5725
|
+
if (stale.length === 0)
|
|
5726
|
+
continue;
|
|
5727
|
+
const staleCommandsByEvent = new Map;
|
|
5728
|
+
for (const finding of stale) {
|
|
5729
|
+
const commands = staleCommandsByEvent.get(finding.event) ?? new Set;
|
|
5730
|
+
commands.add(finding.command);
|
|
5731
|
+
staleCommandsByEvent.set(finding.event, commands);
|
|
5732
|
+
}
|
|
5733
|
+
const settings = parsed.settings;
|
|
5734
|
+
for (const [eventKey, commands] of staleCommandsByEvent) {
|
|
5735
|
+
const entries = settings.hooks?.[eventKey];
|
|
5736
|
+
if (!Array.isArray(entries))
|
|
5737
|
+
continue;
|
|
5738
|
+
const keptEntries = [];
|
|
5739
|
+
for (const entry of entries) {
|
|
5740
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
5741
|
+
keptEntries.push(entry);
|
|
5742
|
+
continue;
|
|
5743
|
+
}
|
|
5744
|
+
const keptHooks = entry.hooks.filter((hook) => !(typeof hook?.command === "string" && commands.has(hook.command)));
|
|
5745
|
+
if (keptHooks.length === entry.hooks.length)
|
|
5746
|
+
keptEntries.push(entry);
|
|
5747
|
+
else if (keptHooks.length > 0)
|
|
5748
|
+
keptEntries.push({ ...entry, hooks: keptHooks });
|
|
5749
|
+
}
|
|
5750
|
+
if (keptEntries.length > 0)
|
|
5751
|
+
settings.hooks[eventKey] = keptEntries;
|
|
5752
|
+
else
|
|
5753
|
+
delete settings.hooks[eventKey];
|
|
5754
|
+
}
|
|
5755
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0)
|
|
5756
|
+
delete settings.hooks;
|
|
5757
|
+
const backupPath = `${path}.bak`;
|
|
5758
|
+
writeFileSync3(backupPath, parsed.raw);
|
|
5759
|
+
writeSettings(settings, scope, target);
|
|
5760
|
+
results.push({ file: path, removed: stale, backupPath });
|
|
5761
|
+
}
|
|
5762
|
+
return results;
|
|
5763
|
+
}
|
|
5518
5764
|
function removeCodewithHookEntry(configText, name) {
|
|
5519
5765
|
const sections = configText.split(/\n\s*\n/);
|
|
5520
5766
|
const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
|
|
@@ -5659,11 +5905,12 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5659
5905
|
registrationsRemaining
|
|
5660
5906
|
};
|
|
5661
5907
|
}
|
|
5662
|
-
var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
|
|
5908
|
+
var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks, JSON_SETTINGS_TARGETS;
|
|
5663
5909
|
var init_installer = __esm(() => {
|
|
5664
5910
|
init_registry();
|
|
5665
5911
|
init_resolve();
|
|
5666
5912
|
init_manifest();
|
|
5913
|
+
init_registration();
|
|
5667
5914
|
init_store();
|
|
5668
5915
|
__dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5669
5916
|
HOOKS_DIR = existsSync10(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
|
|
@@ -5700,53 +5947,9 @@ var init_installer = __esm(() => {
|
|
|
5700
5947
|
}
|
|
5701
5948
|
};
|
|
5702
5949
|
getInstalledHooks = getRegisteredHooks;
|
|
5950
|
+
JSON_SETTINGS_TARGETS = ["claude", "gemini"];
|
|
5703
5951
|
});
|
|
5704
5952
|
|
|
5705
|
-
// src/lib/registration.ts
|
|
5706
|
-
function safeMatcherTest(pattern, value) {
|
|
5707
|
-
try {
|
|
5708
|
-
return new RegExp(pattern).test(value);
|
|
5709
|
-
} catch {
|
|
5710
|
-
return false;
|
|
5711
|
-
}
|
|
5712
|
-
}
|
|
5713
|
-
function countSettingsWiring(settings) {
|
|
5714
|
-
const hooks = settings.hooks;
|
|
5715
|
-
if (!hooks || typeof hooks !== "object")
|
|
5716
|
-
return 0;
|
|
5717
|
-
let count = 0;
|
|
5718
|
-
for (const eventKey of Object.keys(hooks)) {
|
|
5719
|
-
const entries = hooks[eventKey];
|
|
5720
|
-
if (!Array.isArray(entries))
|
|
5721
|
-
continue;
|
|
5722
|
-
for (const entry of entries) {
|
|
5723
|
-
if (Array.isArray(entry?.hooks))
|
|
5724
|
-
count += entry.hooks.length;
|
|
5725
|
-
}
|
|
5726
|
-
}
|
|
5727
|
-
return count;
|
|
5728
|
-
}
|
|
5729
|
-
function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
5730
|
-
const separator = eventSpec.indexOf(":");
|
|
5731
|
-
const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
|
|
5732
|
-
const specMatcher = separator === -1 ? "" : eventSpec.slice(separator + 1);
|
|
5733
|
-
const matcher = specMatcher || hookMatcher || "";
|
|
5734
|
-
const hooks = settings.hooks?.[eventName];
|
|
5735
|
-
if (!Array.isArray(hooks))
|
|
5736
|
-
return false;
|
|
5737
|
-
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
5738
|
-
const match = h?.command?.match(/^hooks run ([\w-]+)/);
|
|
5739
|
-
if (!match || match[1] !== name)
|
|
5740
|
-
return false;
|
|
5741
|
-
if (!matcher)
|
|
5742
|
-
return true;
|
|
5743
|
-
const entryMatcher = typeof entry.matcher === "string" ? entry.matcher : "";
|
|
5744
|
-
if (!entryMatcher)
|
|
5745
|
-
return true;
|
|
5746
|
-
return entryMatcher === matcher || safeMatcherTest(entryMatcher, matcher) || safeMatcherTest(matcher, entryMatcher);
|
|
5747
|
-
}));
|
|
5748
|
-
}
|
|
5749
|
-
|
|
5750
5953
|
// src/lib/redact.ts
|
|
5751
5954
|
function redactText(value) {
|
|
5752
5955
|
let out = value;
|
|
@@ -5917,12 +6120,12 @@ var init_profiles = __esm(() => {
|
|
|
5917
6120
|
init_app_home();
|
|
5918
6121
|
});
|
|
5919
6122
|
|
|
5920
|
-
//
|
|
6123
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
|
|
5921
6124
|
import { isIP as isIP2 } from "net";
|
|
5922
6125
|
import { spawnSync } from "child_process";
|
|
5923
6126
|
import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
|
|
5924
6127
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5925
|
-
import { createRequire } from "module";
|
|
6128
|
+
import { createRequire as createRequire2 } from "module";
|
|
5926
6129
|
import { hostname as osHostname } from "os";
|
|
5927
6130
|
import { isAbsolute as isAbsolute2, join as join12 } from "path";
|
|
5928
6131
|
function envToken(name) {
|
|
@@ -6043,7 +6246,7 @@ function readAppConfigFile(path) {
|
|
|
6043
6246
|
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
6044
6247
|
if (uid !== undefined && before.uid !== uid)
|
|
6045
6248
|
unsafe("the file is not owned by the current user");
|
|
6046
|
-
if (before.size >
|
|
6249
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
|
|
6047
6250
|
unsafe("the file exceeds the size limit");
|
|
6048
6251
|
const bytes = readFileSync6(fd);
|
|
6049
6252
|
const after = fstatSync(fd);
|
|
@@ -6127,13 +6330,13 @@ function sealCredential(fields) {
|
|
|
6127
6330
|
configurable: false
|
|
6128
6331
|
});
|
|
6129
6332
|
}
|
|
6130
|
-
Object.defineProperty(sealed,
|
|
6333
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM2, {
|
|
6131
6334
|
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
6132
6335
|
enumerable: false,
|
|
6133
6336
|
writable: false,
|
|
6134
6337
|
configurable: false
|
|
6135
6338
|
});
|
|
6136
|
-
Object.defineProperty(sealed,
|
|
6339
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
|
|
6137
6340
|
value: true,
|
|
6138
6341
|
enumerable: false,
|
|
6139
6342
|
writable: false,
|
|
@@ -6152,7 +6355,7 @@ function firstEnvValue(env, keys) {
|
|
|
6152
6355
|
return null;
|
|
6153
6356
|
}
|
|
6154
6357
|
function isAmbientEnvironment(env) {
|
|
6155
|
-
return env === process.env || env[
|
|
6358
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
|
|
6156
6359
|
}
|
|
6157
6360
|
function defaultKeychainRunner(argv) {
|
|
6158
6361
|
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
@@ -6246,7 +6449,7 @@ function snapshotClientEnvironment(name, env) {
|
|
|
6246
6449
|
snapshot[key] = descriptor.value;
|
|
6247
6450
|
}
|
|
6248
6451
|
if (ambient) {
|
|
6249
|
-
Object.defineProperty(snapshot,
|
|
6452
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
|
|
6250
6453
|
value: true,
|
|
6251
6454
|
enumerable: false,
|
|
6252
6455
|
writable: false,
|
|
@@ -6597,7 +6800,7 @@ function credentialDiskSourcesForMessage(name, env) {
|
|
|
6597
6800
|
const paths = credentialDiskSources(name, env);
|
|
6598
6801
|
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
6599
6802
|
}
|
|
6600
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4,
|
|
6803
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
|
|
6601
6804
|
var init_transport = __esm(() => {
|
|
6602
6805
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
6603
6806
|
appName;
|
|
@@ -6617,17 +6820,17 @@ var init_transport = __esm(() => {
|
|
|
6617
6820
|
this.path = path;
|
|
6618
6821
|
}
|
|
6619
6822
|
};
|
|
6620
|
-
|
|
6823
|
+
MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
6621
6824
|
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
6622
6825
|
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
6623
6826
|
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
6624
6827
|
VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
6625
6828
|
CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6829
|
+
INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
6830
|
+
CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
6831
|
+
AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
6629
6832
|
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
6630
|
-
|
|
6833
|
+
requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
6631
6834
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
6632
6835
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
6633
6836
|
ClientTransportConfigurationError = class ClientTransportConfigurationError extends Error {
|
|
@@ -6640,8 +6843,8 @@ var init_transport = __esm(() => {
|
|
|
6640
6843
|
this.sources = Object.freeze([...sources]);
|
|
6641
6844
|
}
|
|
6642
6845
|
};
|
|
6643
|
-
|
|
6644
|
-
|
|
6846
|
+
IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
6847
|
+
AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
6645
6848
|
"host",
|
|
6646
6849
|
":authority",
|
|
6647
6850
|
"forwarded",
|
|
@@ -6670,6 +6873,15 @@ function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
|
6670
6873
|
function selectsHooksLocalStore(env = process.env) {
|
|
6671
6874
|
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
6672
6875
|
}
|
|
6876
|
+
function hooksFailClosedLine(detail) {
|
|
6877
|
+
return `REMOTE_API_CONFIG_MISSING: hooks: no registry credential resolved from ${HOOKS_CREDENTIAL_TIERS} ` + `(with HASNA_HOOKS_API_URL or the fleet gateway default), and local mode is not enabled ` + `(${HOOKS_LOCAL_OPT_IN_HINT}) \u2014 refusing to silently fall back to local storage.` + (detail ? ` ${detail}` : "");
|
|
6878
|
+
}
|
|
6879
|
+
function hooksHostedRouteLocalStoreRefusal(authority) {
|
|
6880
|
+
return "REMOTE_COMMAND_UNSUPPORTED: this command is local-only \u2014 it reads the on-box SQLite store " + `(hooks.db), which the hosted hooks registry authority (${authority}) does not serve; local SQLite ` + `is opt-in only (${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default \u2014 refusing to open a local ` + "store on the hosted route.";
|
|
6881
|
+
}
|
|
6882
|
+
function hooksEventSinkRefusal() {
|
|
6883
|
+
return "REMOTE_COMMAND_UNSUPPORTED: hook event not recorded \u2014 the hook event log is local-only (on-box " + "SQLite hooks.db) and the hosted hooks registry has no event route; local SQLite is opt-in only " + `(${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default. Nothing was written.`;
|
|
6884
|
+
}
|
|
6673
6885
|
function hooksResolverEnv(env) {
|
|
6674
6886
|
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
6675
6887
|
if (blanks.length === 0)
|
|
@@ -6694,7 +6906,7 @@ function hooksResolverInputs(env, credentials = {}) {
|
|
|
6694
6906
|
}
|
|
6695
6907
|
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
6696
6908
|
}
|
|
6697
|
-
var HOOKS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
6909
|
+
var HOOKS_LOCAL_OPT_IN_ENV_KEYS, HOOKS_CREDENTIAL_TIERS = "the Keychain item hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY", HOOKS_LOCAL_OPT_IN_HINT = "HASNA_HOOKS_LOCAL=1 (alias HOOKS_LOCAL=1)", CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
6698
6910
|
var init_local_opt_in = __esm(() => {
|
|
6699
6911
|
init_transport();
|
|
6700
6912
|
HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
|
|
@@ -6797,14 +7009,35 @@ var init_transport2 = __esm(() => {
|
|
|
6797
7009
|
var exports_db_writer = {};
|
|
6798
7010
|
__export(exports_db_writer, {
|
|
6799
7011
|
writeHookEvent: () => writeHookEvent,
|
|
7012
|
+
resolveHookEventSink: () => resolveHookEventSink,
|
|
6800
7013
|
resolveEventType: () => resolveEventType,
|
|
6801
7014
|
recordHookRun: () => recordHookRun,
|
|
6802
|
-
normalizeEventType: () => normalizeEventType
|
|
7015
|
+
normalizeEventType: () => normalizeEventType,
|
|
7016
|
+
__resetHookEventSinkNotice: () => __resetHookEventSinkNotice
|
|
6803
7017
|
});
|
|
7018
|
+
function resolveHookEventSink(env = process.env) {
|
|
7019
|
+
if (isLocalStoreRefused())
|
|
7020
|
+
return { kind: "refused", reason: localStoreRefusalMessage() ?? hooksEventSinkRefusal() };
|
|
7021
|
+
if (selectsHooksLocalStore(env))
|
|
7022
|
+
return { kind: "local" };
|
|
7023
|
+
return { kind: "refused", reason: hooksEventSinkRefusal() };
|
|
7024
|
+
}
|
|
7025
|
+
function __resetHookEventSinkNotice() {
|
|
7026
|
+
sinkRefusalPrinted = false;
|
|
7027
|
+
}
|
|
6804
7028
|
function nanoid2() {
|
|
6805
7029
|
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
6806
7030
|
}
|
|
6807
7031
|
function writeHookEvent(event) {
|
|
7032
|
+
const sink = resolveHookEventSink();
|
|
7033
|
+
if (sink.kind !== "local") {
|
|
7034
|
+
if (!sinkRefusalPrinted) {
|
|
7035
|
+
sinkRefusalPrinted = true;
|
|
7036
|
+
process.stderr.write(`[hooks db-writer] ${sink.reason}
|
|
7037
|
+
`);
|
|
7038
|
+
}
|
|
7039
|
+
return;
|
|
7040
|
+
}
|
|
6808
7041
|
try {
|
|
6809
7042
|
const db = getDb();
|
|
6810
7043
|
const id = nanoid2();
|
|
@@ -6873,9 +7106,10 @@ function recordHookRun(record) {
|
|
|
6873
7106
|
metadata: JSON.stringify(metadata)
|
|
6874
7107
|
});
|
|
6875
7108
|
}
|
|
6876
|
-
var ALLOWED_EVENT_TYPES;
|
|
7109
|
+
var sinkRefusalPrinted = false, ALLOWED_EVENT_TYPES;
|
|
6877
7110
|
var init_db_writer = __esm(() => {
|
|
6878
7111
|
init_db();
|
|
7112
|
+
init_local_opt_in();
|
|
6879
7113
|
init_redact();
|
|
6880
7114
|
ALLOWED_EVENT_TYPES = new Set([
|
|
6881
7115
|
"PreToolUse",
|
|
@@ -6891,7 +7125,8 @@ var init_db_writer = __esm(() => {
|
|
|
6891
7125
|
|
|
6892
7126
|
// src/lib/hook-env.ts
|
|
6893
7127
|
import { dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
|
|
6894
|
-
import { existsSync as existsSync12, statSync } from "fs";
|
|
7128
|
+
import { existsSync as existsSync12, realpathSync as realpathSync2, statSync } from "fs";
|
|
7129
|
+
import { tmpdir } from "os";
|
|
6895
7130
|
function isDeniedEnvName(name) {
|
|
6896
7131
|
const upper = name.toUpperCase();
|
|
6897
7132
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -6917,6 +7152,19 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6917
7152
|
return true;
|
|
6918
7153
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
6919
7154
|
return true;
|
|
7155
|
+
const canonical = (path) => {
|
|
7156
|
+
try {
|
|
7157
|
+
return realpathSync2(path);
|
|
7158
|
+
} catch {
|
|
7159
|
+
return path;
|
|
7160
|
+
}
|
|
7161
|
+
};
|
|
7162
|
+
const resolved = canonical(entry);
|
|
7163
|
+
for (const root of [home, tmpdir(), "/tmp", "/var/tmp"].filter((value) => Boolean(value))) {
|
|
7164
|
+
const blocked = canonical(root);
|
|
7165
|
+
if (resolved === blocked || resolved.startsWith(blocked + "/"))
|
|
7166
|
+
return true;
|
|
7167
|
+
}
|
|
6920
7168
|
try {
|
|
6921
7169
|
if ((statSync(entry).mode & 2) !== 0)
|
|
6922
7170
|
return true;
|
|
@@ -7036,6 +7284,7 @@ var init_hook_env = __esm(() => {
|
|
|
7036
7284
|
DENY_CONTAINS = ["DATABASE_URL"];
|
|
7037
7285
|
PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
|
|
7038
7286
|
CONFIG_PROJECTIONS = [
|
|
7287
|
+
["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"],
|
|
7039
7288
|
["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
|
|
7040
7289
|
["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
|
|
7041
7290
|
["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
|
|
@@ -7215,7 +7464,7 @@ __export(exports_custom_install, {
|
|
|
7215
7464
|
});
|
|
7216
7465
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
7217
7466
|
import { join as join14 } from "path";
|
|
7218
|
-
import { tmpdir } from "os";
|
|
7467
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
7219
7468
|
function manifestAt(dir) {
|
|
7220
7469
|
const path = join14(dir, "manifest.json");
|
|
7221
7470
|
if (!existsSync13(path))
|
|
@@ -7240,7 +7489,7 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7240
7489
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7241
7490
|
}
|
|
7242
7491
|
async function cloneToTemp(gitUrl) {
|
|
7243
|
-
const tmp = mkdtempSync(join14(
|
|
7492
|
+
const tmp = mkdtempSync(join14(tmpdir2(), "hooks-install-"));
|
|
7244
7493
|
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join14(tmp, "repo")], {
|
|
7245
7494
|
stdout: "pipe",
|
|
7246
7495
|
stderr: "pipe"
|
|
@@ -7327,6 +7576,7 @@ __export(exports_sync, {
|
|
|
7327
7576
|
syncHooks: () => syncHooks,
|
|
7328
7577
|
stageSyncArtifacts: () => stageSyncArtifacts,
|
|
7329
7578
|
planSync: () => planSync,
|
|
7579
|
+
installPinnedFromBundled: () => installPinnedFromBundled,
|
|
7330
7580
|
fetchPinnedHook: () => fetchPinnedHook,
|
|
7331
7581
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
7332
7582
|
});
|
|
@@ -7490,7 +7740,7 @@ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
|
7490
7740
|
return staged;
|
|
7491
7741
|
}
|
|
7492
7742
|
async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
7493
|
-
const db = getDb();
|
|
7743
|
+
const db = isLocalStoreRefused() ? null : getDb();
|
|
7494
7744
|
for (const item of staged) {
|
|
7495
7745
|
const entry = remoteLock.hooks[item.name];
|
|
7496
7746
|
if (!entry)
|
|
@@ -7510,6 +7760,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
7510
7760
|
lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
|
|
7511
7761
|
}
|
|
7512
7762
|
writeLock(lock);
|
|
7763
|
+
if (!db)
|
|
7764
|
+
return;
|
|
7513
7765
|
db.exec("BEGIN");
|
|
7514
7766
|
try {
|
|
7515
7767
|
for (const item of staged) {
|
|
@@ -7572,23 +7824,56 @@ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
|
|
|
7572
7824
|
if (actual !== expectedSha) {
|
|
7573
7825
|
throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
|
|
7574
7826
|
}
|
|
7575
|
-
const db = getDb();
|
|
7576
7827
|
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
7577
|
-
|
|
7828
|
+
if (!isLocalStoreRefused()) {
|
|
7829
|
+
upsertHookRecord(getDb(), {
|
|
7830
|
+
name,
|
|
7831
|
+
version,
|
|
7832
|
+
sha256: expectedSha,
|
|
7833
|
+
source_type: entry.source ?? "remote",
|
|
7834
|
+
source_ref: apiUrl,
|
|
7835
|
+
last_verified_at: new Date().toISOString()
|
|
7836
|
+
});
|
|
7837
|
+
}
|
|
7838
|
+
return {
|
|
7578
7839
|
name,
|
|
7579
7840
|
version,
|
|
7580
7841
|
sha256: expectedSha,
|
|
7581
|
-
|
|
7842
|
+
source: entry.source ?? "remote",
|
|
7582
7843
|
source_ref: apiUrl,
|
|
7844
|
+
artifact,
|
|
7845
|
+
scriptPath
|
|
7846
|
+
};
|
|
7847
|
+
}
|
|
7848
|
+
async function installPinnedFromBundled(name, version) {
|
|
7849
|
+
const bundled = collectBundledCatalog().find((entry) => entry.name === name);
|
|
7850
|
+
if (!bundled) {
|
|
7851
|
+
throw new Error(`Hook '${name}' is not in the bundled registry`);
|
|
7852
|
+
}
|
|
7853
|
+
if (bundled.version !== version) {
|
|
7854
|
+
throw new Error(`Hook '${name}' version ${version} is not in the bundled registry (available: ${bundled.version})`);
|
|
7855
|
+
}
|
|
7856
|
+
const scriptPath = resolveScriptPath(name);
|
|
7857
|
+
if (!scriptPath) {
|
|
7858
|
+
throw new Error(`Hook '${name}' has no script in the bundled registry`);
|
|
7859
|
+
}
|
|
7860
|
+
const db = getDb();
|
|
7861
|
+
setPinnedHook(name, { version, sha256: bundled.sha256, source: "bundled", pinned: true });
|
|
7862
|
+
upsertHookRecord(db, {
|
|
7863
|
+
name,
|
|
7864
|
+
version,
|
|
7865
|
+
sha256: bundled.sha256,
|
|
7866
|
+
source_type: "bundled",
|
|
7867
|
+
source_ref: "bundled registry",
|
|
7583
7868
|
last_verified_at: new Date().toISOString()
|
|
7584
7869
|
});
|
|
7585
7870
|
return {
|
|
7586
7871
|
name,
|
|
7587
7872
|
version,
|
|
7588
|
-
sha256:
|
|
7589
|
-
source:
|
|
7590
|
-
source_ref:
|
|
7591
|
-
artifact,
|
|
7873
|
+
sha256: bundled.sha256,
|
|
7874
|
+
source: "bundled",
|
|
7875
|
+
source_ref: "bundled registry",
|
|
7876
|
+
artifact: null,
|
|
7592
7877
|
scriptPath
|
|
7593
7878
|
};
|
|
7594
7879
|
}
|
|
@@ -8421,7 +8706,7 @@ function startServeServer(options) {
|
|
|
8421
8706
|
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
8422
8707
|
}
|
|
8423
8708
|
});
|
|
8424
|
-
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
8709
|
+
console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
|
|
8425
8710
|
return server;
|
|
8426
8711
|
}
|
|
8427
8712
|
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
@@ -8517,6 +8802,32 @@ async function provisionCloudflareResources(options) {
|
|
|
8517
8802
|
}
|
|
8518
8803
|
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
8519
8804
|
|
|
8805
|
+
// src/mcp/authority.ts
|
|
8806
|
+
var exports_authority = {};
|
|
8807
|
+
__export(exports_authority, {
|
|
8808
|
+
resolveHooksMcpAuthority: () => resolveHooksMcpAuthority,
|
|
8809
|
+
decideHooksMcpAuthority: () => decideHooksMcpAuthority
|
|
8810
|
+
});
|
|
8811
|
+
function resolveHooksMcpAuthority(env = process.env) {
|
|
8812
|
+
const transport = resolveHooksTransport(env);
|
|
8813
|
+
if (transport.mode === "local" || !transport.authority) {
|
|
8814
|
+
return { route: "local", source: "local-opt-in" };
|
|
8815
|
+
}
|
|
8816
|
+
return { route: "hosted", v1BaseUrl: transport.authority.v1BaseUrl, source: transport.source };
|
|
8817
|
+
}
|
|
8818
|
+
function decideHooksMcpAuthority(env = process.env) {
|
|
8819
|
+
const authority = resolveHooksMcpAuthority(env);
|
|
8820
|
+
if (authority.route === "hosted") {
|
|
8821
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(authority.v1BaseUrl));
|
|
8822
|
+
}
|
|
8823
|
+
return authority;
|
|
8824
|
+
}
|
|
8825
|
+
var init_authority = __esm(() => {
|
|
8826
|
+
init_db();
|
|
8827
|
+
init_local_opt_in();
|
|
8828
|
+
init_transport2();
|
|
8829
|
+
});
|
|
8830
|
+
|
|
8520
8831
|
// src/mcp/server.ts
|
|
8521
8832
|
var exports_server = {};
|
|
8522
8833
|
__export(exports_server, {
|
|
@@ -8785,6 +9096,13 @@ function createHooksServer() {
|
|
|
8785
9096
|
if (hookHealthy)
|
|
8786
9097
|
healthy.push(name);
|
|
8787
9098
|
}
|
|
9099
|
+
for (const overlap of findRewriteOverlaps(registered, (name) => getHook(name))) {
|
|
9100
|
+
issues.push({
|
|
9101
|
+
hook: overlap.hooks.join(" + "),
|
|
9102
|
+
issue: `Both rewrite the tool input on overlapping ${overlap.event} matchers ('${overlap.matchers[0]}' / '${overlap.matchers[1]}'); only one rewrite is applied per tool call, so one of these guards is silently disarmed. Remove one, or narrow a matcher so they no longer overlap.`,
|
|
9103
|
+
severity: "error"
|
|
9104
|
+
});
|
|
9105
|
+
}
|
|
8788
9106
|
return { content: [{ type: "text", text: JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }) }] };
|
|
8789
9107
|
});
|
|
8790
9108
|
defineTool("hooks_categories", "List all hook categories with counts", {}, async () => {
|
|
@@ -9451,6 +9769,7 @@ async function startSSEServer(options = {}) {
|
|
|
9451
9769
|
if (!loopback && !authToken) {
|
|
9452
9770
|
throw new Error(`Refusing to serve MCP SSE on non-loopback host '${host}' without an auth token. Set HASNA_HOOKS_MCP_TOKEN (or HOOKS_MCP_TOKEN) to authorize a non-loopback bind.`);
|
|
9453
9771
|
}
|
|
9772
|
+
decideHooksMcpAuthority();
|
|
9454
9773
|
const server = createHooksServer();
|
|
9455
9774
|
const transports = new Map;
|
|
9456
9775
|
function authorized2(headers) {
|
|
@@ -9500,6 +9819,13 @@ async function startSSEServer(options = {}) {
|
|
|
9500
9819
|
});
|
|
9501
9820
|
}
|
|
9502
9821
|
async function startStdioServer() {
|
|
9822
|
+
try {
|
|
9823
|
+
decideHooksMcpAuthority();
|
|
9824
|
+
} catch (err) {
|
|
9825
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
9826
|
+
`);
|
|
9827
|
+
process.exit(1);
|
|
9828
|
+
}
|
|
9503
9829
|
try {
|
|
9504
9830
|
const server = createHooksServer();
|
|
9505
9831
|
const transport = new StdioServerTransport;
|
|
@@ -9517,8 +9843,10 @@ var init_server = __esm(() => {
|
|
|
9517
9843
|
init_profiles();
|
|
9518
9844
|
init_manifest();
|
|
9519
9845
|
init_resolve();
|
|
9846
|
+
init_registration();
|
|
9520
9847
|
init_store();
|
|
9521
9848
|
init_redact();
|
|
9849
|
+
init_authority();
|
|
9522
9850
|
init_storage();
|
|
9523
9851
|
__dirname3 = dirname7(fileURLToPath4(import.meta.url));
|
|
9524
9852
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
@@ -9612,6 +9940,7 @@ import { spawn } from "child_process";
|
|
|
9612
9940
|
import { request as nodeHttpRequest } from "http";
|
|
9613
9941
|
import { request as nodeHttpsRequest } from "https";
|
|
9614
9942
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
9943
|
+
import { createRequire } from "module";
|
|
9615
9944
|
function getPathValue(input, path) {
|
|
9616
9945
|
return path.split(".").reduce((value, part) => {
|
|
9617
9946
|
if (value && typeof value === "object" && part in value) {
|
|
@@ -10709,6 +11038,22 @@ function setPath(input, path, replacement) {
|
|
|
10709
11038
|
if (last && last in cursor)
|
|
10710
11039
|
cursor[last] = replacement;
|
|
10711
11040
|
}
|
|
11041
|
+
var MAX_PACKAGE_BYTES = 1024 * 1024;
|
|
11042
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
11043
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
11044
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
11045
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
11046
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
11047
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
11048
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
11049
|
+
"host",
|
|
11050
|
+
":authority",
|
|
11051
|
+
"forwarded",
|
|
11052
|
+
"x-forwarded-host",
|
|
11053
|
+
"x-original-host"
|
|
11054
|
+
]);
|
|
11055
|
+
var MAX_ENVELOPE_BYTES = 256 * 1024;
|
|
11056
|
+
var MAX_REQUEST_BYTES = MAX_ENVELOPE_BYTES * 2 + 8192;
|
|
10712
11057
|
function createEvent(input) {
|
|
10713
11058
|
return {
|
|
10714
11059
|
id: input.id ?? randomUUID2(),
|
|
@@ -12427,6 +12772,7 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
12427
12772
|
// src/cli/index.tsx
|
|
12428
12773
|
init_registry();
|
|
12429
12774
|
init_installer();
|
|
12775
|
+
init_registration();
|
|
12430
12776
|
init_redact();
|
|
12431
12777
|
init_profiles();
|
|
12432
12778
|
init_manifest();
|
|
@@ -12437,6 +12783,7 @@ init_app_home();
|
|
|
12437
12783
|
init_config();
|
|
12438
12784
|
init_local_opt_in();
|
|
12439
12785
|
init_transport2();
|
|
12786
|
+
init_db();
|
|
12440
12787
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
12441
12788
|
var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
|
|
12442
12789
|
var pkgPath = existsSync15(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
|
|
@@ -12471,6 +12818,19 @@ function truncateText2(value, max = 96) {
|
|
|
12471
12818
|
function projectLogRows2(rows) {
|
|
12472
12819
|
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
12473
12820
|
}
|
|
12821
|
+
async function openLocalStoreOrExit(json2) {
|
|
12822
|
+
try {
|
|
12823
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
12824
|
+
return getDb2();
|
|
12825
|
+
} catch (error) {
|
|
12826
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12827
|
+
if (json2)
|
|
12828
|
+
console.log(JSON.stringify({ error: message }));
|
|
12829
|
+
else
|
|
12830
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
12831
|
+
process.exit(1);
|
|
12832
|
+
}
|
|
12833
|
+
}
|
|
12474
12834
|
function readToken(tokenFile) {
|
|
12475
12835
|
if (tokenFile) {
|
|
12476
12836
|
try {
|
|
@@ -12535,6 +12895,11 @@ function suggestHooks(name, max = 3) {
|
|
|
12535
12895
|
}
|
|
12536
12896
|
program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
|
|
12537
12897
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
12898
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
12899
|
+
console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
|
|
12900
|
+
` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
|
|
12901
|
+
process.exit(1);
|
|
12902
|
+
}
|
|
12538
12903
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
12539
12904
|
});
|
|
12540
12905
|
program2.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) => {
|
|
@@ -12770,6 +13135,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
12770
13135
|
const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12771
13136
|
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
12772
13137
|
const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
13138
|
+
const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12773
13139
|
const { readFileSync: readFileSyncFs } = await import("fs");
|
|
12774
13140
|
function parseNameVersion(arg) {
|
|
12775
13141
|
if (isCustomSource2(arg))
|
|
@@ -12862,17 +13228,13 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
12862
13228
|
for (const { arg, pinned } of pinnedRequests) {
|
|
12863
13229
|
try {
|
|
12864
13230
|
const transport = resolveHooksTransport2();
|
|
12865
|
-
|
|
12866
|
-
throw new Error(`Cannot install '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
12867
|
-
}
|
|
12868
|
-
const { origin, apiKey } = transport.authority;
|
|
12869
|
-
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12870
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
13231
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
12871
13232
|
installedPinned.push(pinnedInstall.name);
|
|
12872
13233
|
if (options.json) {
|
|
12873
13234
|
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
12874
13235
|
} else {
|
|
12875
|
-
|
|
13236
|
+
const registryLabel = transport.mode === "remote" ? "registry" : "bundled registry";
|
|
13237
|
+
console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from ${registryLabel} (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
|
|
12876
13238
|
}
|
|
12877
13239
|
} catch (error) {
|
|
12878
13240
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -13192,11 +13554,21 @@ ${meta.displayName}
|
|
|
13192
13554
|
console.log(chalk2.dim(" \u25CB Not installed in project"));
|
|
13193
13555
|
}
|
|
13194
13556
|
});
|
|
13195
|
-
program2.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks").action((options) => {
|
|
13557
|
+
program2.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) => {
|
|
13196
13558
|
const scope = resolveScope(options);
|
|
13197
13559
|
const settingsPath = getSettingsPath(scope);
|
|
13198
13560
|
const issues = [];
|
|
13199
13561
|
const healthy = [];
|
|
13562
|
+
const fixed = options.fix ? pruneStaleRegistrations(scope) : [];
|
|
13563
|
+
const stale = scanStaleRegistrations(scope);
|
|
13564
|
+
for (const finding of stale) {
|
|
13565
|
+
issues.push({
|
|
13566
|
+
hook: finding.hook,
|
|
13567
|
+
issue: `Stale registration in ${finding.file} (${finding.event}): \`${finding.command}\` does not resolve to a hook`,
|
|
13568
|
+
severity: "error"
|
|
13569
|
+
});
|
|
13570
|
+
}
|
|
13571
|
+
const staleNames = new Set(stale.map((finding) => finding.hook));
|
|
13200
13572
|
const settingsExist = existsSync15(settingsPath);
|
|
13201
13573
|
if (!settingsExist) {
|
|
13202
13574
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
@@ -13210,6 +13582,8 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13210
13582
|
} catch {}
|
|
13211
13583
|
}
|
|
13212
13584
|
for (const name of registered) {
|
|
13585
|
+
if (staleNames.has(name))
|
|
13586
|
+
continue;
|
|
13213
13587
|
const custom = readCustomManifest(name);
|
|
13214
13588
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
13215
13589
|
let hookHealthy = true;
|
|
@@ -13245,17 +13619,53 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13245
13619
|
healthy.push(name);
|
|
13246
13620
|
}
|
|
13247
13621
|
}
|
|
13622
|
+
for (const overlap of findRewriteOverlaps(registered, (name) => getHook(name))) {
|
|
13623
|
+
issues.push({
|
|
13624
|
+
hook: overlap.hooks.join(" + "),
|
|
13625
|
+
issue: `Both rewrite the tool input on overlapping ${overlap.event} matchers ('${overlap.matchers[0]}' / '${overlap.matchers[1]}'); only one rewrite is applied per tool call, so one of these guards is silently disarmed. Remove one, or narrow a matcher so they no longer overlap.`,
|
|
13626
|
+
severity: "error"
|
|
13627
|
+
});
|
|
13628
|
+
}
|
|
13248
13629
|
if (options.json) {
|
|
13249
|
-
console.log(JSON.stringify({
|
|
13630
|
+
console.log(JSON.stringify({
|
|
13631
|
+
healthy: issues.length === 0,
|
|
13632
|
+
healthy_hooks: healthy,
|
|
13633
|
+
issues,
|
|
13634
|
+
stale,
|
|
13635
|
+
fixed: fixed.flatMap((result) => result.removed),
|
|
13636
|
+
backups: fixed.map((result) => result.backupPath),
|
|
13637
|
+
registered,
|
|
13638
|
+
wiring_count: wiringCount,
|
|
13639
|
+
scope
|
|
13640
|
+
}));
|
|
13641
|
+
if (issues.some((issue) => issue.severity === "error"))
|
|
13642
|
+
process.exitCode = 1;
|
|
13250
13643
|
return;
|
|
13251
13644
|
}
|
|
13252
13645
|
console.log(chalk2.bold(`
|
|
13253
13646
|
Hook Health Check (${scope})
|
|
13254
13647
|
`));
|
|
13648
|
+
if (fixed.length > 0) {
|
|
13649
|
+
for (const result of fixed) {
|
|
13650
|
+
console.log(chalk2.green(` \u2713 removed ${result.removed.length} stale registration(s) from ${result.file}`));
|
|
13651
|
+
for (const finding of result.removed) {
|
|
13652
|
+
console.log(chalk2.dim(` ${finding.hook} (${finding.event}: ${finding.command})`));
|
|
13653
|
+
}
|
|
13654
|
+
console.log(chalk2.dim(` backup: ${result.backupPath}`));
|
|
13655
|
+
}
|
|
13656
|
+
console.log();
|
|
13657
|
+
}
|
|
13255
13658
|
if (registered.length === 0) {
|
|
13256
13659
|
console.log(chalk2.dim(" No hooks registered."));
|
|
13257
13660
|
console.log(chalk2.dim(" Run: hooks install gitguard"));
|
|
13258
13661
|
console.log(chalk2.dim(` (checked 0 registered \`hooks run\` entries of ${wiringCount} settings wiring entries; direct-path wiring outside the registered surface is not covered by this check)`));
|
|
13662
|
+
if (issues.length > 0) {
|
|
13663
|
+
console.log();
|
|
13664
|
+
for (const issue of issues) {
|
|
13665
|
+
const icon = issue.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
13666
|
+
console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
|
|
13667
|
+
}
|
|
13668
|
+
}
|
|
13259
13669
|
if (issues.length > 0)
|
|
13260
13670
|
process.exitCode = 1;
|
|
13261
13671
|
return;
|
|
@@ -13299,8 +13709,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13299
13709
|
}
|
|
13300
13710
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
13301
13711
|
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
13302
|
-
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
13712
|
+
const { getDb: getDb2, isLocalStoreRefused: isLocalStoreRefused2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
13303
13713
|
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
13714
|
+
const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13304
13715
|
const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
13305
13716
|
const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
13306
13717
|
function parseNameVersion(arg) {
|
|
@@ -13323,12 +13734,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13323
13734
|
if (pinned) {
|
|
13324
13735
|
try {
|
|
13325
13736
|
const transport = resolveHooksTransport2();
|
|
13326
|
-
|
|
13327
|
-
throw new Error(`Cannot update '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
13328
|
-
}
|
|
13329
|
-
const { origin, apiKey } = transport.authority;
|
|
13330
|
-
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13331
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
13737
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
13332
13738
|
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
13333
13739
|
if (!registered.success) {
|
|
13334
13740
|
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
@@ -13353,13 +13759,15 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13353
13759
|
if (result.success && resolved) {
|
|
13354
13760
|
const hash = await sha256File2(resolved.scriptPath);
|
|
13355
13761
|
setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13762
|
+
if (!isLocalStoreRefused2()) {
|
|
13763
|
+
upsertHookRecord2(getDb2(), {
|
|
13764
|
+
name: resolved.name,
|
|
13765
|
+
version: resolved.version,
|
|
13766
|
+
sha256: hash,
|
|
13767
|
+
source_type: resolved.source,
|
|
13768
|
+
last_verified_at: new Date().toISOString()
|
|
13769
|
+
});
|
|
13770
|
+
}
|
|
13363
13771
|
results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
|
|
13364
13772
|
continue;
|
|
13365
13773
|
}
|
|
@@ -13637,8 +14045,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
13637
14045
|
});
|
|
13638
14046
|
var logCmd = program2.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
|
|
13639
14047
|
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) => {
|
|
13640
|
-
const
|
|
13641
|
-
const db = getDb2();
|
|
14048
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13642
14049
|
const limit = parseInt(options.limit) || 50;
|
|
13643
14050
|
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
13644
14051
|
const params = [];
|
|
@@ -13675,8 +14082,7 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
|
|
|
13675
14082
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
13676
14083
|
});
|
|
13677
14084
|
logCmd.command("search <text>").description("Search hook events by tool_input or error text").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (text, options) => {
|
|
13678
|
-
const
|
|
13679
|
-
const db = getDb2();
|
|
14085
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13680
14086
|
const limit = parseInt(options.limit) || 50;
|
|
13681
14087
|
const q = `%${text}%`;
|
|
13682
14088
|
const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
|
|
@@ -13701,8 +14107,7 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
|
|
|
13701
14107
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
13702
14108
|
});
|
|
13703
14109
|
logCmd.command("tail").description("Show most recent hook events").option("-n <n>", "Number of rows", "20").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13704
|
-
const
|
|
13705
|
-
const db = getDb2();
|
|
14110
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13706
14111
|
const limit = parseInt(options.n) || 20;
|
|
13707
14112
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
13708
14113
|
const projected = projectLogRows2(rows);
|
|
@@ -13727,8 +14132,7 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
|
|
|
13727
14132
|
Compact rows shown. Use --json for full event records or -n <n> to change row count.`));
|
|
13728
14133
|
});
|
|
13729
14134
|
logCmd.command("errors").description("Show hook events that contain errors").option("--since <duration>", "Only show errors since this duration (e.g. 1h, 30m, 7d)", "24h").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13730
|
-
const
|
|
13731
|
-
const db = getDb2();
|
|
14135
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13732
14136
|
const limit = parseInt(options.limit) || 50;
|
|
13733
14137
|
function parseDuration(s) {
|
|
13734
14138
|
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
@@ -13770,8 +14174,7 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
|
|
|
13770
14174
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
13771
14175
|
});
|
|
13772
14176
|
logCmd.command("clear").description("Delete hook event logs").option("--hook <name>", "Only delete events for this hook").option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
|
|
13773
|
-
const
|
|
13774
|
-
const db = getDb2();
|
|
14177
|
+
const db = await openLocalStoreOrExit(false);
|
|
13775
14178
|
const countRow = options.hook ? db.query("SELECT COUNT(*) as n FROM hook_events WHERE hook_name = ?").get(options.hook) : db.query("SELECT COUNT(*) as n FROM hook_events").get();
|
|
13776
14179
|
const count = countRow?.n ?? 0;
|
|
13777
14180
|
if (count === 0) {
|
|
@@ -13793,19 +14196,28 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
|
|
|
13793
14196
|
});
|
|
13794
14197
|
var storageCmd = program2.command("storage").description("Sync local hook data with storage PostgreSQL");
|
|
13795
14198
|
storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
14199
|
+
try {
|
|
14200
|
+
const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14201
|
+
const status = getStorageStatus2();
|
|
14202
|
+
if (options.json) {
|
|
14203
|
+
console.log(JSON.stringify(status, null, 2));
|
|
14204
|
+
return;
|
|
14205
|
+
}
|
|
14206
|
+
console.log(chalk2.bold(`
|
|
13803
14207
|
Storage Status
|
|
13804
14208
|
`));
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
14209
|
+
console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
|
|
14210
|
+
console.log(` Backend: ${status.backend}`);
|
|
14211
|
+
console.log(` Tables: ${status.tables.join(", ")}`);
|
|
14212
|
+
console.log(` Sync rows: ${status.sync.length}`);
|
|
14213
|
+
} catch (error) {
|
|
14214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14215
|
+
if (options.json)
|
|
14216
|
+
console.log(JSON.stringify({ error: message }));
|
|
14217
|
+
else
|
|
14218
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
14219
|
+
process.exitCode = 1;
|
|
14220
|
+
}
|
|
13809
14221
|
});
|
|
13810
14222
|
storageCmd.command("push").description("Push local hook data to storage PostgreSQL").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13811
14223
|
try {
|
|
@@ -14015,6 +14427,13 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14015
14427
|
const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14016
14428
|
await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
|
|
14017
14429
|
} else {
|
|
14430
|
+
const { decideHooksMcpAuthority: decideHooksMcpAuthority2 } = await Promise.resolve().then(() => (init_authority(), exports_authority));
|
|
14431
|
+
try {
|
|
14432
|
+
decideHooksMcpAuthority2();
|
|
14433
|
+
} catch (error) {
|
|
14434
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
14435
|
+
process.exit(1);
|
|
14436
|
+
}
|
|
14018
14437
|
const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14019
14438
|
const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
14020
14439
|
const args = options.port ? ["--port", options.port] : [];
|
|
@@ -14023,7 +14442,6 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14023
14442
|
});
|
|
14024
14443
|
registerEventsCommands(program2, { source: "hooks" });
|
|
14025
14444
|
var API_INDEPENDENT_COMMANDS = new Set([
|
|
14026
|
-
"run",
|
|
14027
14445
|
"serve",
|
|
14028
14446
|
"mcp",
|
|
14029
14447
|
"cf",
|
|
@@ -14035,11 +14453,11 @@ var API_INDEPENDENT_COMMANDS = new Set([
|
|
|
14035
14453
|
"events"
|
|
14036
14454
|
]);
|
|
14037
14455
|
function failClosedForMissingApiEnv(detail) {
|
|
14038
|
-
console.error(chalk2.red(
|
|
14456
|
+
console.error(chalk2.red(hooksFailClosedLine() + `
|
|
14039
14457
|
` + `The remote registry requires a STRICT pair \u2014 set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or the
|
|
14040
14458
|
` + `Keychain items hasna.credentials.hooks.api-url / .api-key, or ~/.hasna/hooks/config/credentials.
|
|
14041
|
-
|
|
14042
|
-
|
|
14459
|
+
config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
|
|
14460
|
+
HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
|
|
14043
14461
|
` + (detail ? `${detail}
|
|
14044
14462
|
` : "") + "Refusing to silently fall back to local storage."));
|
|
14045
14463
|
process.exit(1);
|
|
@@ -14063,21 +14481,25 @@ function enforceTransportGate() {
|
|
|
14063
14481
|
return;
|
|
14064
14482
|
if (token !== null && API_INDEPENDENT_COMMANDS.has(token))
|
|
14065
14483
|
return;
|
|
14066
|
-
if (hasHooksEnvAuthorityIntent(process.env))
|
|
14484
|
+
if (hasHooksEnvAuthorityIntent(process.env)) {
|
|
14485
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal("configured by HASNA_HOOKS_API_URL / HASNA_HOOKS_API_KEY"));
|
|
14067
14486
|
return;
|
|
14487
|
+
}
|
|
14068
14488
|
if (isHooksLocalOptIn(process.env)) {
|
|
14069
14489
|
announceHooksLocalMode();
|
|
14070
14490
|
return;
|
|
14071
14491
|
}
|
|
14492
|
+
let transport;
|
|
14072
14493
|
try {
|
|
14073
|
-
|
|
14074
|
-
if (transport.mode !== "remote" || !transport.authority) {
|
|
14075
|
-
failClosedForMissingApiEnv();
|
|
14076
|
-
}
|
|
14494
|
+
transport = resolveHooksTransport(process.env);
|
|
14077
14495
|
} catch (error) {
|
|
14078
14496
|
const message = error instanceof Error ? error.message : String(error);
|
|
14079
14497
|
failClosedForMissingApiEnv(message);
|
|
14080
14498
|
}
|
|
14499
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
14500
|
+
failClosedForMissingApiEnv();
|
|
14501
|
+
}
|
|
14502
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
|
|
14081
14503
|
}
|
|
14082
14504
|
enforceTransportGate();
|
|
14083
14505
|
program2.parse();
|