@hasna/hooks 0.6.5 → 0.6.8
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 +1 -1
- package/bin/index.js +697 -181
- package/bin/serve.js +111 -28
- package/dist/config.d.ts +8 -1
- package/dist/db/remote-storage.d.ts +22 -0
- package/dist/index.js +506 -113
- package/dist/lib/hook-env.d.ts +98 -0
- package/dist/lib/manifest.d.ts +35 -2
- package/dist/lib/redact.d.ts +40 -0
- package/dist/lib/semver.d.ts +46 -0
- package/dist/lib/store.d.ts +16 -0
- package/dist/lib/sync.d.ts +55 -4
- package/dist/serve.d.ts +2 -1
- package/dist/storage.js +104 -7
- package/package.json +2 -2
package/bin/index.js
CHANGED
|
@@ -4119,24 +4119,30 @@ function resolveApiUrl() {
|
|
|
4119
4119
|
const cfg = readConfig();
|
|
4120
4120
|
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4121
4121
|
}
|
|
4122
|
-
function resolveApiKey(
|
|
4123
|
-
if (flagKey && flagKey.length > 0)
|
|
4124
|
-
return flagKey;
|
|
4122
|
+
function resolveApiKey() {
|
|
4125
4123
|
return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
|
|
4126
4124
|
}
|
|
4127
4125
|
var init_config = () => {};
|
|
4128
4126
|
|
|
4127
|
+
// src/lib/semver.ts
|
|
4128
|
+
var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
|
|
4129
|
+
var init_semver = __esm(() => {
|
|
4130
|
+
SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
|
|
4131
|
+
});
|
|
4132
|
+
|
|
4129
4133
|
// src/lib/manifest.ts
|
|
4130
4134
|
var exports_manifest = {};
|
|
4131
4135
|
__export(exports_manifest, {
|
|
4132
4136
|
writeCustomHook: () => writeCustomHook,
|
|
4133
4137
|
shortManifestName: () => shortManifestName,
|
|
4138
|
+
scriptRelFor: () => scriptRelFor,
|
|
4134
4139
|
resolveScript: () => resolveScript,
|
|
4135
4140
|
readCustomManifest: () => readCustomManifest,
|
|
4136
4141
|
parseManifest: () => parseManifest,
|
|
4137
4142
|
normalizeManifestName: () => normalizeManifestName,
|
|
4138
4143
|
manifestSchema: () => manifestSchema,
|
|
4139
4144
|
listCustomHooks: () => listCustomHooks,
|
|
4145
|
+
isInlineScript: () => isInlineScript,
|
|
4140
4146
|
customHookDir: () => customHookDir,
|
|
4141
4147
|
assertContained: () => assertContained,
|
|
4142
4148
|
ScriptContainmentError: () => ScriptContainmentError,
|
|
@@ -4190,16 +4196,25 @@ function assertContained(target, root, raw) {
|
|
|
4190
4196
|
assertContainedReal(root, lexical, raw);
|
|
4191
4197
|
return lexical;
|
|
4192
4198
|
}
|
|
4193
|
-
function
|
|
4194
|
-
|
|
4195
|
-
`)
|
|
4199
|
+
function isInlineScript(manifest) {
|
|
4200
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4201
|
+
`);
|
|
4202
|
+
}
|
|
4203
|
+
function scriptRelFor(manifest) {
|
|
4204
|
+
if (isInlineScript(manifest)) {
|
|
4196
4205
|
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4197
|
-
return
|
|
4206
|
+
return `script${ext}`;
|
|
4198
4207
|
}
|
|
4199
4208
|
const rel = normalize(manifest.script);
|
|
4200
4209
|
if (isAbsolute(rel)) {
|
|
4201
4210
|
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4202
4211
|
}
|
|
4212
|
+
return rel;
|
|
4213
|
+
}
|
|
4214
|
+
function resolveScript(manifest, manifestDir) {
|
|
4215
|
+
if (isInlineScript(manifest))
|
|
4216
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4217
|
+
const rel = scriptRelFor(manifest);
|
|
4203
4218
|
const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
|
|
4204
4219
|
if (!existsSync3(resolved)) {
|
|
4205
4220
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
@@ -4217,8 +4232,8 @@ function readCustomManifest(name) {
|
|
|
4217
4232
|
try {
|
|
4218
4233
|
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
4219
4234
|
const script = resolveScript(manifest, dir);
|
|
4220
|
-
|
|
4221
|
-
|
|
4235
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4236
|
+
return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4222
4237
|
} catch (err) {
|
|
4223
4238
|
if (err instanceof ScriptContainmentError)
|
|
4224
4239
|
throw err;
|
|
@@ -4264,15 +4279,18 @@ function readdirSafe(dir) {
|
|
|
4264
4279
|
var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
|
|
4265
4280
|
var init_manifest = __esm(() => {
|
|
4266
4281
|
init_config();
|
|
4282
|
+
init_semver();
|
|
4267
4283
|
HOOK_NAME_RE = /^[\w-]+$/;
|
|
4268
4284
|
manifestSchema = z.object({
|
|
4269
4285
|
name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4270
|
-
version: z.string().regex(
|
|
4286
|
+
version: z.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4271
4287
|
description: z.string().optional(),
|
|
4272
4288
|
events: z.array(z.string()).min(1, "at least one event is required"),
|
|
4273
4289
|
script: z.string().min(1, "script must name a relative path or inline content"),
|
|
4290
|
+
script_kind: z.enum(["inline", "file"]).optional(),
|
|
4274
4291
|
args: z.array(z.string()).optional(),
|
|
4275
|
-
timeout_ms: z.number().int().positive().optional()
|
|
4292
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
4293
|
+
env: z.record(z.string(), z.string()).optional()
|
|
4276
4294
|
});
|
|
4277
4295
|
ScriptContainmentError = class ScriptContainmentError extends Error {
|
|
4278
4296
|
constructor(path, hookDir, raw) {
|
|
@@ -4799,11 +4817,13 @@ __export(exports_store, {
|
|
|
4799
4817
|
listHookRecords: () => listHookRecords,
|
|
4800
4818
|
getPinnedHook: () => getPinnedHook,
|
|
4801
4819
|
getHookRecord: () => getHookRecord,
|
|
4802
|
-
checkScriptHash: () => checkScriptHash
|
|
4820
|
+
checkScriptHash: () => checkScriptHash,
|
|
4821
|
+
LockFileError: () => LockFileError
|
|
4803
4822
|
});
|
|
4804
4823
|
import { createHash } from "crypto";
|
|
4805
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4824
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
4806
4825
|
import { readFile as readFile2 } from "fs/promises";
|
|
4826
|
+
import { randomBytes } from "crypto";
|
|
4807
4827
|
function sha256Of(input) {
|
|
4808
4828
|
return createHash("sha256").update(input).digest("hex");
|
|
4809
4829
|
}
|
|
@@ -4846,16 +4866,39 @@ function removeHookRecord(db, name) {
|
|
|
4846
4866
|
const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
|
|
4847
4867
|
return res.changes > 0;
|
|
4848
4868
|
}
|
|
4869
|
+
function assertLockShape(parsed, path) {
|
|
4870
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4871
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: expected a JSON object with a "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4872
|
+
}
|
|
4873
|
+
const hooks = parsed.hooks;
|
|
4874
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
4875
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: missing or invalid "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4876
|
+
}
|
|
4877
|
+
for (const [name, entry] of Object.entries(hooks)) {
|
|
4878
|
+
const e = entry;
|
|
4879
|
+
if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
|
|
4880
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: entry for '${name}' is not a valid pin (version and sha256 strings required). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it.`);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4849
4884
|
function readLock() {
|
|
4885
|
+
const path = getLockPath();
|
|
4886
|
+
if (!existsSync7(path))
|
|
4887
|
+
return { hooks: {} };
|
|
4888
|
+
let raw;
|
|
4850
4889
|
try {
|
|
4851
|
-
|
|
4852
|
-
if (!existsSync7(path))
|
|
4853
|
-
return { hooks: {} };
|
|
4854
|
-
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4855
|
-
return { hooks: parsed.hooks ?? {} };
|
|
4890
|
+
raw = readFileSync4(path, "utf-8");
|
|
4856
4891
|
} catch {
|
|
4857
|
-
|
|
4892
|
+
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
4893
|
+
}
|
|
4894
|
+
let parsed;
|
|
4895
|
+
try {
|
|
4896
|
+
parsed = JSON.parse(raw);
|
|
4897
|
+
} catch {
|
|
4898
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: not valid JSON (${String((raw ?? "").slice(0, 80))}). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4858
4899
|
}
|
|
4900
|
+
assertLockShape(parsed, path);
|
|
4901
|
+
return parsed;
|
|
4859
4902
|
}
|
|
4860
4903
|
function writeLock(lock) {
|
|
4861
4904
|
const path = getLockPath();
|
|
@@ -4864,8 +4907,10 @@ function writeLock(lock) {
|
|
|
4864
4907
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
4865
4908
|
sorted[name] = lock.hooks[name];
|
|
4866
4909
|
}
|
|
4867
|
-
|
|
4910
|
+
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
4911
|
+
writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
4868
4912
|
`, "utf-8");
|
|
4913
|
+
renameSync(tmp, path);
|
|
4869
4914
|
return path;
|
|
4870
4915
|
}
|
|
4871
4916
|
function setPinnedHook(name, entry) {
|
|
@@ -4951,9 +4996,16 @@ function retrustHook(name, scriptPath, version, source) {
|
|
|
4951
4996
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
4952
4997
|
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
4953
4998
|
}
|
|
4999
|
+
var LockFileError;
|
|
4954
5000
|
var init_store = __esm(() => {
|
|
4955
5001
|
init_db();
|
|
4956
5002
|
init_config();
|
|
5003
|
+
LockFileError = class LockFileError extends Error {
|
|
5004
|
+
constructor(message) {
|
|
5005
|
+
super(message);
|
|
5006
|
+
this.name = "LockFileError";
|
|
5007
|
+
}
|
|
5008
|
+
};
|
|
4957
5009
|
});
|
|
4958
5010
|
|
|
4959
5011
|
// src/lib/installer.ts
|
|
@@ -5006,7 +5058,7 @@ function getGlobalSettingsPathOverride(target) {
|
|
|
5006
5058
|
function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
|
|
5007
5059
|
if (target === "codewith" && codewithConfigPath)
|
|
5008
5060
|
return codewithConfigPath;
|
|
5009
|
-
if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
5061
|
+
if (target === "codewith" && scope === "global" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
5010
5062
|
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
|
|
5011
5063
|
}
|
|
5012
5064
|
const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
|
|
@@ -5439,8 +5491,8 @@ function escapeRegExp(value) {
|
|
|
5439
5491
|
function codewithHasHookEntryInText(configText, name) {
|
|
5440
5492
|
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
5441
5493
|
}
|
|
5442
|
-
function codewithHasHookEntry(name) {
|
|
5443
|
-
const path = getSettingsPath(
|
|
5494
|
+
function codewithHasHookEntry(name, scope = "global") {
|
|
5495
|
+
const path = getSettingsPath(scope, "codewith");
|
|
5444
5496
|
if (!existsSync8(path))
|
|
5445
5497
|
return false;
|
|
5446
5498
|
try {
|
|
@@ -5457,7 +5509,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5457
5509
|
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
5458
5510
|
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
5459
5511
|
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
5460
|
-
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
|
|
5512
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
5461
5513
|
if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
5462
5514
|
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
5463
5515
|
}
|
|
@@ -5474,7 +5526,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5474
5526
|
}
|
|
5475
5527
|
const registrationsRemaining = [];
|
|
5476
5528
|
if (target === "codewith" || target === "all") {
|
|
5477
|
-
const codewithPath = getSettingsPath(
|
|
5529
|
+
const codewithPath = getSettingsPath(scope, "codewith");
|
|
5478
5530
|
if (existsSync8(codewithPath)) {
|
|
5479
5531
|
const before = readFileSync5(codewithPath, "utf-8");
|
|
5480
5532
|
const after = removeCodewithHookEntry(before, shortName);
|
|
@@ -5566,6 +5618,22 @@ function safeMatcherTest(pattern, value) {
|
|
|
5566
5618
|
return false;
|
|
5567
5619
|
}
|
|
5568
5620
|
}
|
|
5621
|
+
function countSettingsWiring(settings) {
|
|
5622
|
+
const hooks = settings.hooks;
|
|
5623
|
+
if (!hooks || typeof hooks !== "object")
|
|
5624
|
+
return 0;
|
|
5625
|
+
let count = 0;
|
|
5626
|
+
for (const eventKey of Object.keys(hooks)) {
|
|
5627
|
+
const entries = hooks[eventKey];
|
|
5628
|
+
if (!Array.isArray(entries))
|
|
5629
|
+
continue;
|
|
5630
|
+
for (const entry of entries) {
|
|
5631
|
+
if (Array.isArray(entry?.hooks))
|
|
5632
|
+
count += entry.hooks.length;
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
return count;
|
|
5636
|
+
}
|
|
5569
5637
|
function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
5570
5638
|
const separator = eventSpec.indexOf(":");
|
|
5571
5639
|
const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
|
|
@@ -5587,6 +5655,71 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
5587
5655
|
}));
|
|
5588
5656
|
}
|
|
5589
5657
|
|
|
5658
|
+
// src/lib/redact.ts
|
|
5659
|
+
function redactText(value) {
|
|
5660
|
+
let out = value;
|
|
5661
|
+
for (const pattern of SECRET_SHAPES) {
|
|
5662
|
+
out = out.replace(pattern, REDACTED);
|
|
5663
|
+
}
|
|
5664
|
+
return out;
|
|
5665
|
+
}
|
|
5666
|
+
function redactValue2(value) {
|
|
5667
|
+
if (typeof value === "string")
|
|
5668
|
+
return redactText(value);
|
|
5669
|
+
if (Array.isArray(value))
|
|
5670
|
+
return value.map(redactValue2);
|
|
5671
|
+
if (value !== null && typeof value === "object") {
|
|
5672
|
+
const out = {};
|
|
5673
|
+
for (const [key, child] of Object.entries(value)) {
|
|
5674
|
+
out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue2(child);
|
|
5675
|
+
}
|
|
5676
|
+
return out;
|
|
5677
|
+
}
|
|
5678
|
+
return value;
|
|
5679
|
+
}
|
|
5680
|
+
function redactEventPayload(text) {
|
|
5681
|
+
if (text === null || text === undefined)
|
|
5682
|
+
return null;
|
|
5683
|
+
try {
|
|
5684
|
+
const parsed = JSON.parse(text);
|
|
5685
|
+
return redactText(JSON.stringify(redactValue2(parsed)));
|
|
5686
|
+
} catch {
|
|
5687
|
+
return redactText(text);
|
|
5688
|
+
}
|
|
5689
|
+
}
|
|
5690
|
+
function projectEventRowForRead(row) {
|
|
5691
|
+
const out = { ...row };
|
|
5692
|
+
for (const field of ["tool_input", "error", "metadata"]) {
|
|
5693
|
+
const value = out[field];
|
|
5694
|
+
if (typeof value === "string")
|
|
5695
|
+
out[field] = redactEventPayload(value);
|
|
5696
|
+
else if (value === null)
|
|
5697
|
+
out[field] = null;
|
|
5698
|
+
}
|
|
5699
|
+
return out;
|
|
5700
|
+
}
|
|
5701
|
+
var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
|
|
5702
|
+
var init_redact = __esm(() => {
|
|
5703
|
+
SECRET_KEY_NAMES = /^(key|keys|token|tokens|secret|secrets|password|passwd|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|refresh[_-]?token|authorization|auth|cookie|credential|credentials|session[_-]?id|otp|mfa[_-]?code|two[_-]?fa[_-]?code|verification[_-]?code|database_url|db_url|connection_string|dsn)$/i;
|
|
5704
|
+
SECRET_SHAPES = [
|
|
5705
|
+
/\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
|
|
5706
|
+
/\bsk-[A-Za-z0-9]{16,}/g,
|
|
5707
|
+
/\btpe_[A-Za-z0-9]{8,}/g,
|
|
5708
|
+
/\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
5709
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
5710
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
5711
|
+
/xox[baprs]-[A-Za-z0-9-]{20,}/g,
|
|
5712
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
5713
|
+
/ASIA[0-9A-Z]{16}/g,
|
|
5714
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
5715
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
5716
|
+
/-----BEGIN OPENSSH PRIVATE KEY-----/g,
|
|
5717
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
|
|
5718
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
|
|
5719
|
+
/\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
|
|
5720
|
+
];
|
|
5721
|
+
});
|
|
5722
|
+
|
|
5590
5723
|
// src/lib/profiles.ts
|
|
5591
5724
|
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
5592
5725
|
import { join as join8 } from "path";
|
|
@@ -5708,6 +5841,9 @@ function writeHookEvent(event) {
|
|
|
5708
5841
|
const db = getDb();
|
|
5709
5842
|
const id = nanoid2();
|
|
5710
5843
|
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
5844
|
+
const toolInput = redactEventPayload(event.tool_input);
|
|
5845
|
+
const error = redactEventPayload(event.error);
|
|
5846
|
+
const metadata = redactEventPayload(event.metadata);
|
|
5711
5847
|
db.run(`INSERT INTO hook_events
|
|
5712
5848
|
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5713
5849
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
@@ -5717,12 +5853,12 @@ function writeHookEvent(event) {
|
|
|
5717
5853
|
event.hook_name,
|
|
5718
5854
|
event.event_type,
|
|
5719
5855
|
event.tool_name ?? null,
|
|
5720
|
-
|
|
5856
|
+
toolInput ? toolInput.slice(0, 500) : null,
|
|
5721
5857
|
event.result ?? null,
|
|
5722
|
-
|
|
5858
|
+
error ?? null,
|
|
5723
5859
|
event.duration_ms ?? null,
|
|
5724
5860
|
event.project_dir ?? null,
|
|
5725
|
-
|
|
5861
|
+
metadata ?? null
|
|
5726
5862
|
]);
|
|
5727
5863
|
} catch (err) {
|
|
5728
5864
|
process.stderr.write(`[hooks db-writer] failed to write event: ${err}
|
|
@@ -5772,6 +5908,7 @@ function recordHookRun(record) {
|
|
|
5772
5908
|
var ALLOWED_EVENT_TYPES;
|
|
5773
5909
|
var init_db_writer = __esm(() => {
|
|
5774
5910
|
init_db();
|
|
5911
|
+
init_redact();
|
|
5775
5912
|
ALLOWED_EVENT_TYPES = new Set([
|
|
5776
5913
|
"PreToolUse",
|
|
5777
5914
|
"PostToolUse",
|
|
@@ -5784,15 +5921,169 @@ var init_db_writer = __esm(() => {
|
|
|
5784
5921
|
]);
|
|
5785
5922
|
});
|
|
5786
5923
|
|
|
5924
|
+
// src/lib/hook-env.ts
|
|
5925
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
|
|
5926
|
+
import { existsSync as existsSync10, statSync } from "fs";
|
|
5927
|
+
function isDeniedEnvName(name) {
|
|
5928
|
+
const upper = name.toUpperCase();
|
|
5929
|
+
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
5930
|
+
return true;
|
|
5931
|
+
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
5932
|
+
return true;
|
|
5933
|
+
return DENY_CONTAINS.some((token) => upper.includes(token));
|
|
5934
|
+
}
|
|
5935
|
+
function isInterpreterInjectionEnvName(name) {
|
|
5936
|
+
if (INTERPRETER_INJECTION.has(name))
|
|
5937
|
+
return true;
|
|
5938
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
5939
|
+
}
|
|
5940
|
+
function isBashFunctionEnvName(name) {
|
|
5941
|
+
return name.startsWith("BASH_FUNC_");
|
|
5942
|
+
}
|
|
5943
|
+
function isUnsafePathEntry(entry, home) {
|
|
5944
|
+
if (entry === "" || !isAbsolute2(entry))
|
|
5945
|
+
return true;
|
|
5946
|
+
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
5947
|
+
return true;
|
|
5948
|
+
if (entry === "/tmp" || entry.startsWith("/tmp/"))
|
|
5949
|
+
return true;
|
|
5950
|
+
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
5951
|
+
return true;
|
|
5952
|
+
try {
|
|
5953
|
+
if ((statSync(entry).mode & 2) !== 0)
|
|
5954
|
+
return true;
|
|
5955
|
+
} catch {}
|
|
5956
|
+
return false;
|
|
5957
|
+
}
|
|
5958
|
+
function sanitizeHookPath(pathValue, home) {
|
|
5959
|
+
const baseline = PATH_BASELINE.slice();
|
|
5960
|
+
if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
|
|
5961
|
+
baseline.push("/opt/homebrew/bin");
|
|
5962
|
+
const bunDir = dirname4(process.execPath);
|
|
5963
|
+
const seen = new Set;
|
|
5964
|
+
const out = [];
|
|
5965
|
+
for (const dir of baseline) {
|
|
5966
|
+
if (seen.has(dir))
|
|
5967
|
+
continue;
|
|
5968
|
+
seen.add(dir);
|
|
5969
|
+
out.push(dir);
|
|
5970
|
+
}
|
|
5971
|
+
if (!seen.has(bunDir)) {
|
|
5972
|
+
seen.add(bunDir);
|
|
5973
|
+
out.push(bunDir);
|
|
5974
|
+
}
|
|
5975
|
+
for (const entry of pathValue.split(":")) {
|
|
5976
|
+
if (seen.has(entry))
|
|
5977
|
+
continue;
|
|
5978
|
+
if (isUnsafePathEntry(entry, home))
|
|
5979
|
+
continue;
|
|
5980
|
+
seen.add(entry);
|
|
5981
|
+
out.push(entry);
|
|
5982
|
+
}
|
|
5983
|
+
return out.join(":");
|
|
5984
|
+
}
|
|
5985
|
+
function buildHookEnv(source, extra) {
|
|
5986
|
+
const out = {};
|
|
5987
|
+
for (const name of ALLOWLIST) {
|
|
5988
|
+
if (name === "PATH")
|
|
5989
|
+
continue;
|
|
5990
|
+
const value = source[name] ?? extra?.[name];
|
|
5991
|
+
if (value !== undefined && value !== null)
|
|
5992
|
+
out[name] = value;
|
|
5993
|
+
}
|
|
5994
|
+
const pathOverride = extra?.PATH;
|
|
5995
|
+
if (pathOverride !== undefined && pathOverride !== null) {
|
|
5996
|
+
out.PATH = pathOverride;
|
|
5997
|
+
} else {
|
|
5998
|
+
const parentPath = source.PATH;
|
|
5999
|
+
if (parentPath !== undefined && parentPath !== null) {
|
|
6000
|
+
out.PATH = sanitizeHookPath(parentPath, source.HOME ?? extra?.HOME);
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
for (const [from, to] of CONFIG_PROJECTIONS) {
|
|
6004
|
+
const value = source[from] ?? extra?.[from];
|
|
6005
|
+
if (value !== undefined && value !== null)
|
|
6006
|
+
out[to] = value;
|
|
6007
|
+
}
|
|
6008
|
+
const merged = { ...source, ...extra ?? {} };
|
|
6009
|
+
for (const [name, value] of Object.entries(merged)) {
|
|
6010
|
+
if (out[name] !== undefined)
|
|
6011
|
+
continue;
|
|
6012
|
+
if (isDeniedEnvName(name))
|
|
6013
|
+
continue;
|
|
6014
|
+
if (isInterpreterInjectionEnvName(name))
|
|
6015
|
+
continue;
|
|
6016
|
+
if (isBashFunctionEnvName(name))
|
|
6017
|
+
continue;
|
|
6018
|
+
if (value !== undefined && value !== null)
|
|
6019
|
+
out[name] = value;
|
|
6020
|
+
}
|
|
6021
|
+
return out;
|
|
6022
|
+
}
|
|
6023
|
+
var ALLOWLIST, INTERPRETER_INJECTION, INTERPRETER_INJECTION_PREFIXES, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, PATH_BASELINE, CONFIG_PROJECTIONS;
|
|
6024
|
+
var init_hook_env = __esm(() => {
|
|
6025
|
+
ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
|
|
6026
|
+
INTERPRETER_INJECTION = new Set([
|
|
6027
|
+
"BASH_ENV",
|
|
6028
|
+
"ENV",
|
|
6029
|
+
"BASHOPTS",
|
|
6030
|
+
"SHELLOPTS",
|
|
6031
|
+
"NODE_OPTIONS",
|
|
6032
|
+
"NODE_PATH",
|
|
6033
|
+
"PYTHONSTARTUP",
|
|
6034
|
+
"PYTHONINSPECT",
|
|
6035
|
+
"PYTHONPATH",
|
|
6036
|
+
"PYTHONHOME",
|
|
6037
|
+
"LD_PRELOAD",
|
|
6038
|
+
"LD_LIBRARY_PATH",
|
|
6039
|
+
"GCONV_PATH",
|
|
6040
|
+
"LOCPATH",
|
|
6041
|
+
"PERL5OPT",
|
|
6042
|
+
"RUBYOPT",
|
|
6043
|
+
"NODE_EXTRA_CA_CERTS",
|
|
6044
|
+
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
6045
|
+
"SSL_CERT_FILE",
|
|
6046
|
+
"SSL_CERT_DIR",
|
|
6047
|
+
"CURL_CA_BUNDLE",
|
|
6048
|
+
"GIT_SSL_CAINFO",
|
|
6049
|
+
"AWS_CA_BUNDLE"
|
|
6050
|
+
]);
|
|
6051
|
+
INTERPRETER_INJECTION_PREFIXES = ["GIT_CONFIG_"];
|
|
6052
|
+
DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
|
|
6053
|
+
DENY_PREFIXES = [
|
|
6054
|
+
"HASNA_",
|
|
6055
|
+
"AWS_",
|
|
6056
|
+
"AZURE_",
|
|
6057
|
+
"GCP_",
|
|
6058
|
+
"VAULT_",
|
|
6059
|
+
"GOOGLE_",
|
|
6060
|
+
"OPENAI_",
|
|
6061
|
+
"ANTHROPIC_",
|
|
6062
|
+
"POSTGRES_",
|
|
6063
|
+
"MYSQL_",
|
|
6064
|
+
"REDIS_",
|
|
6065
|
+
"MONGO_",
|
|
6066
|
+
"MEMENTOS_"
|
|
6067
|
+
];
|
|
6068
|
+
DENY_CONTAINS = ["DATABASE_URL"];
|
|
6069
|
+
PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
|
|
6070
|
+
CONFIG_PROJECTIONS = [
|
|
6071
|
+
["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
|
|
6072
|
+
["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
|
|
6073
|
+
["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
|
|
6074
|
+
["HASNA_HOOKS_CONFIG_PATH", "HOOKS_CONFIG_PATH"]
|
|
6075
|
+
];
|
|
6076
|
+
});
|
|
6077
|
+
|
|
5787
6078
|
// src/lib/run.ts
|
|
5788
6079
|
var exports_run = {};
|
|
5789
6080
|
__export(exports_run, {
|
|
5790
6081
|
executeVerifiedScript: () => executeVerifiedScript,
|
|
5791
6082
|
HookTimeoutError: () => HookTimeoutError
|
|
5792
6083
|
});
|
|
5793
|
-
import { randomBytes } from "crypto";
|
|
6084
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
5794
6085
|
import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
5795
|
-
import { basename as basename2, dirname as
|
|
6086
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join9 } from "path";
|
|
5796
6087
|
function extensionOf(scriptPath) {
|
|
5797
6088
|
const base = basename2(scriptPath);
|
|
5798
6089
|
const dot = base.lastIndexOf(".");
|
|
@@ -5819,13 +6110,13 @@ function interpreterFor(name, scriptPath, content) {
|
|
|
5819
6110
|
if (interp === "bash" || interp === "sh")
|
|
5820
6111
|
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
5821
6112
|
if (interp === "node" || interp === "bun")
|
|
5822
|
-
return { command: [
|
|
6113
|
+
return { command: [process.execPath, "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
|
|
5823
6114
|
throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
|
|
5824
6115
|
}
|
|
5825
6116
|
if (BASH_EXTENSIONS.has(ext))
|
|
5826
6117
|
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
5827
6118
|
if (BUN_EXTENSIONS.has(ext))
|
|
5828
|
-
return { command: [
|
|
6119
|
+
return { command: [process.execPath, "run"], tempExt: ext };
|
|
5829
6120
|
throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
|
|
5830
6121
|
}
|
|
5831
6122
|
async function readPipeWithDeadline(stream, deadlineMs) {
|
|
@@ -5861,9 +6152,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
5861
6152
|
return chunks.join("");
|
|
5862
6153
|
}
|
|
5863
6154
|
async function executeVerifiedScript(options) {
|
|
5864
|
-
const scriptDir =
|
|
6155
|
+
const scriptDir = dirname5(options.scriptPath);
|
|
5865
6156
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
5866
|
-
const tempPath = join9(scriptDir, `.hooks-verified-${
|
|
6157
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
5867
6158
|
let proc = null;
|
|
5868
6159
|
let timedOut = false;
|
|
5869
6160
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -5872,11 +6163,11 @@ async function executeVerifiedScript(options) {
|
|
|
5872
6163
|
}, options.timeout) : null;
|
|
5873
6164
|
try {
|
|
5874
6165
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
5875
|
-
proc = Bun.spawn([...interpreter.command,
|
|
6166
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
5876
6167
|
stdin: new Response(options.stdin),
|
|
5877
6168
|
stdout: "pipe",
|
|
5878
6169
|
stderr: "pipe",
|
|
5879
|
-
env:
|
|
6170
|
+
env: buildHookEnv(process.env, options.env),
|
|
5880
6171
|
detached: true
|
|
5881
6172
|
});
|
|
5882
6173
|
const exitCode = await proc.exited;
|
|
@@ -5934,6 +6225,7 @@ function killGroup(proc, options) {
|
|
|
5934
6225
|
}
|
|
5935
6226
|
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5936
6227
|
var init_run = __esm(() => {
|
|
6228
|
+
init_hook_env();
|
|
5937
6229
|
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5938
6230
|
timeoutMs;
|
|
5939
6231
|
constructor(timeoutMs) {
|
|
@@ -5953,12 +6245,12 @@ __export(exports_custom_install, {
|
|
|
5953
6245
|
installCustomSource: () => installCustomSource,
|
|
5954
6246
|
ensureCustomDir: () => ensureCustomDir
|
|
5955
6247
|
});
|
|
5956
|
-
import { existsSync as
|
|
6248
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
|
|
5957
6249
|
import { join as join10 } from "path";
|
|
5958
6250
|
import { tmpdir } from "os";
|
|
5959
6251
|
function manifestAt(dir) {
|
|
5960
6252
|
const path = join10(dir, "manifest.json");
|
|
5961
|
-
if (!
|
|
6253
|
+
if (!existsSync11(path))
|
|
5962
6254
|
return;
|
|
5963
6255
|
try {
|
|
5964
6256
|
return parseManifest(readFileSync7(path, "utf-8"));
|
|
@@ -6004,7 +6296,7 @@ function isCustomSource(value) {
|
|
|
6004
6296
|
return true;
|
|
6005
6297
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
6006
6298
|
return true;
|
|
6007
|
-
return
|
|
6299
|
+
return existsSync11(value);
|
|
6008
6300
|
}
|
|
6009
6301
|
async function installCustomSource(source) {
|
|
6010
6302
|
if (isGitUrl(source)) {
|
|
@@ -6018,7 +6310,7 @@ async function installCustomSource(source) {
|
|
|
6018
6310
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
6019
6311
|
return installManifestUrl(source);
|
|
6020
6312
|
}
|
|
6021
|
-
if (!
|
|
6313
|
+
if (!existsSync11(source)) {
|
|
6022
6314
|
throw new Error(`No such local path: ${source}`);
|
|
6023
6315
|
}
|
|
6024
6316
|
if (source.endsWith("manifest.json")) {
|
|
@@ -6028,19 +6320,20 @@ async function installCustomSource(source) {
|
|
|
6028
6320
|
return copyManifestDir(source, source, "local");
|
|
6029
6321
|
}
|
|
6030
6322
|
async function installManifestUrl(url) {
|
|
6031
|
-
const res = await fetch(url, { redirect: "
|
|
6323
|
+
const res = await fetch(url, { redirect: "error" });
|
|
6032
6324
|
if (!res.ok) {
|
|
6033
6325
|
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6034
6326
|
}
|
|
6035
6327
|
const manifest = parseManifest(await res.text());
|
|
6036
6328
|
let script;
|
|
6037
|
-
|
|
6038
|
-
`)
|
|
6329
|
+
const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
6330
|
+
`);
|
|
6331
|
+
if (inline) {
|
|
6039
6332
|
script = { path: "script.ts", content: manifest.script };
|
|
6040
6333
|
} else {
|
|
6041
6334
|
const base = url.substring(0, url.lastIndexOf("/"));
|
|
6042
6335
|
const scriptUrl = `${base}/${manifest.script}`;
|
|
6043
|
-
const scriptRes = await fetch(scriptUrl, { redirect: "
|
|
6336
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "error" });
|
|
6044
6337
|
if (!scriptRes.ok) {
|
|
6045
6338
|
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
6046
6339
|
}
|
|
@@ -6064,8 +6357,10 @@ var init_custom_install = __esm(() => {
|
|
|
6064
6357
|
var exports_sync = {};
|
|
6065
6358
|
__export(exports_sync, {
|
|
6066
6359
|
syncHooks: () => syncHooks,
|
|
6360
|
+
stageSyncArtifacts: () => stageSyncArtifacts,
|
|
6067
6361
|
planSync: () => planSync,
|
|
6068
|
-
fetchPinnedHook: () => fetchPinnedHook
|
|
6362
|
+
fetchPinnedHook: () => fetchPinnedHook,
|
|
6363
|
+
commitSyncArtifacts: () => commitSyncArtifacts
|
|
6069
6364
|
});
|
|
6070
6365
|
import { createHash as createHash2 } from "crypto";
|
|
6071
6366
|
import { readFileSync as readFileSync8 } from "fs";
|
|
@@ -6089,7 +6384,7 @@ function collectBundledCatalog() {
|
|
|
6089
6384
|
}
|
|
6090
6385
|
return out;
|
|
6091
6386
|
}
|
|
6092
|
-
async function fetchJson(base, path) {
|
|
6387
|
+
async function fetchJson(base, path, opts) {
|
|
6093
6388
|
const headers = { accept: "application/json" };
|
|
6094
6389
|
const apiKey = resolveApiKey();
|
|
6095
6390
|
if (apiKey)
|
|
@@ -6105,7 +6400,11 @@ async function fetchJson(base, path) {
|
|
|
6105
6400
|
if (!res.ok) {
|
|
6106
6401
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6107
6402
|
}
|
|
6108
|
-
|
|
6403
|
+
const body = await res.json();
|
|
6404
|
+
if (opts?.withHeader) {
|
|
6405
|
+
body[opts.withHeader] = res.headers.get("x-hook-sha256");
|
|
6406
|
+
}
|
|
6407
|
+
return body;
|
|
6109
6408
|
}
|
|
6110
6409
|
async function fetchRemoteState(apiUrl) {
|
|
6111
6410
|
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
@@ -6118,6 +6417,16 @@ async function fetchRemoteState(apiUrl) {
|
|
|
6118
6417
|
}
|
|
6119
6418
|
return { catalog: catalog.hooks, remoteLock };
|
|
6120
6419
|
}
|
|
6420
|
+
function classifyRemotePin(pin, remoteVersion, remoteSha) {
|
|
6421
|
+
const pinnedLocally = pin !== undefined && pin.version === remoteVersion && pin.sha256 === remoteSha;
|
|
6422
|
+
if (pinnedLocally)
|
|
6423
|
+
return "unchanged";
|
|
6424
|
+
if (pin !== undefined && pin.pinned === true && pin.version !== remoteVersion)
|
|
6425
|
+
return "unchanged";
|
|
6426
|
+
if (pin !== undefined)
|
|
6427
|
+
return "updated";
|
|
6428
|
+
return "added";
|
|
6429
|
+
}
|
|
6121
6430
|
function computeDiff(catalog, remoteLock) {
|
|
6122
6431
|
const local = readLock();
|
|
6123
6432
|
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
@@ -6125,30 +6434,30 @@ function computeDiff(catalog, remoteLock) {
|
|
|
6125
6434
|
if (!entry.name || !entry.version || !entry.sha256)
|
|
6126
6435
|
continue;
|
|
6127
6436
|
const pin = local.hooks[entry.name];
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
diff.
|
|
6437
|
+
if (remoteLock) {
|
|
6438
|
+
const remotePin = remoteLock.hooks[entry.name];
|
|
6439
|
+
if (!remotePin) {
|
|
6440
|
+
diff.skipped.push(entry.name);
|
|
6441
|
+
continue;
|
|
6442
|
+
}
|
|
6443
|
+
if (remotePin.version !== entry.version) {
|
|
6444
|
+
throw new Error(`remote registry is ambiguous for '${entry.name}': lock pins ${remotePin.version} but catalog latest is ${entry.version}. ` + `Fix the registry (or its lock) before syncing \u2014 nothing was changed.`);
|
|
6445
|
+
}
|
|
6446
|
+
diff[classifyRemotePin(pin, remotePin.version, remotePin.sha256)].push(entry.name);
|
|
6138
6447
|
} else {
|
|
6139
|
-
diff.
|
|
6448
|
+
diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
|
|
6140
6449
|
}
|
|
6141
6450
|
}
|
|
6142
6451
|
return diff;
|
|
6143
6452
|
}
|
|
6144
|
-
async function planSync() {
|
|
6453
|
+
async function planSync(options = {}) {
|
|
6145
6454
|
const apiUrl = resolveApiUrl();
|
|
6146
6455
|
if (apiUrl) {
|
|
6147
6456
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6148
|
-
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6457
|
+
return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
6149
6458
|
}
|
|
6150
6459
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6151
|
-
return { apiUrl: null, dryRun: false, diff };
|
|
6460
|
+
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
6152
6461
|
}
|
|
6153
6462
|
async function syncHooks(options = {}) {
|
|
6154
6463
|
const apiUrl = resolveApiUrl();
|
|
@@ -6156,10 +6465,10 @@ async function syncHooks(options = {}) {
|
|
|
6156
6465
|
if (options.dryRun)
|
|
6157
6466
|
return { ...plan, dryRun: true };
|
|
6158
6467
|
if (!apiUrl) {
|
|
6159
|
-
const
|
|
6468
|
+
const db = getDb();
|
|
6160
6469
|
for (const entry of collectBundledCatalog()) {
|
|
6161
6470
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6162
|
-
upsertHookRecord(
|
|
6471
|
+
upsertHookRecord(db, {
|
|
6163
6472
|
name: entry.name,
|
|
6164
6473
|
version: entry.version,
|
|
6165
6474
|
sha256: entry.sha256,
|
|
@@ -6172,13 +6481,17 @@ async function syncHooks(options = {}) {
|
|
|
6172
6481
|
}
|
|
6173
6482
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6174
6483
|
const diff = computeDiff(catalog, remoteLock);
|
|
6175
|
-
const
|
|
6176
|
-
|
|
6177
|
-
|
|
6484
|
+
const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
|
|
6485
|
+
await commitSyncArtifacts(staged, apiUrl, remoteLock);
|
|
6486
|
+
return { apiUrl, dryRun: false, diff };
|
|
6487
|
+
}
|
|
6488
|
+
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
6489
|
+
const staged = [];
|
|
6490
|
+
for (const name of names) {
|
|
6178
6491
|
const entry = remoteLock.hooks[name];
|
|
6179
6492
|
if (!entry)
|
|
6180
6493
|
continue;
|
|
6181
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6494
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
|
|
6182
6495
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6183
6496
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6184
6497
|
}
|
|
@@ -6186,34 +6499,64 @@ async function syncHooks(options = {}) {
|
|
|
6186
6499
|
if (actualSha !== entry.sha256) {
|
|
6187
6500
|
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6188
6501
|
}
|
|
6189
|
-
artifacts.set(name, artifact);
|
|
6190
|
-
}
|
|
6191
|
-
const db = getDb();
|
|
6192
|
-
for (const name of toFetch) {
|
|
6193
|
-
const artifact = artifacts.get(name);
|
|
6194
|
-
const entry = remoteLock.hooks[name];
|
|
6195
|
-
if (!artifact || !entry)
|
|
6196
|
-
continue;
|
|
6197
6502
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6198
|
-
|
|
6199
|
-
`
|
|
6200
|
-
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6201
|
-
const scriptPath = resolveScriptPath(name);
|
|
6202
|
-
const actual = await sha256File(scriptPath);
|
|
6203
|
-
if (actual !== entry.sha256) {
|
|
6204
|
-
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6503
|
+
if (manifest.version !== entry.version) {
|
|
6504
|
+
throw new Error(`artifact for '${name}@${entry.version}' declares a different version ('${manifest.version}')`);
|
|
6205
6505
|
}
|
|
6206
|
-
|
|
6207
|
-
|
|
6506
|
+
const scriptRel = scriptRelFor(manifest);
|
|
6507
|
+
staged.push({
|
|
6208
6508
|
name,
|
|
6209
6509
|
version: entry.version,
|
|
6210
6510
|
sha256: entry.sha256,
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6511
|
+
source: entry.source ?? "remote",
|
|
6512
|
+
manifest,
|
|
6513
|
+
scriptContent: artifact.script,
|
|
6514
|
+
scriptRel
|
|
6214
6515
|
});
|
|
6215
6516
|
}
|
|
6216
|
-
return
|
|
6517
|
+
return staged;
|
|
6518
|
+
}
|
|
6519
|
+
async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
6520
|
+
const db = getDb();
|
|
6521
|
+
for (const item of staged) {
|
|
6522
|
+
const entry = remoteLock.hooks[item.name];
|
|
6523
|
+
if (!entry)
|
|
6524
|
+
continue;
|
|
6525
|
+
writeCustomHook(item.name, item.manifest, item.scriptContent, item.scriptRel);
|
|
6526
|
+
const scriptPath = resolveScriptPath(item.name);
|
|
6527
|
+
const actual = await sha256File(scriptPath);
|
|
6528
|
+
if (actual !== entry.sha256) {
|
|
6529
|
+
throw new Error(`sha256 mismatch after write for '${item.name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
const lock = readLock();
|
|
6533
|
+
for (const item of staged) {
|
|
6534
|
+
const entry = remoteLock.hooks[item.name];
|
|
6535
|
+
if (!entry)
|
|
6536
|
+
continue;
|
|
6537
|
+
lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
|
|
6538
|
+
}
|
|
6539
|
+
writeLock(lock);
|
|
6540
|
+
db.exec("BEGIN");
|
|
6541
|
+
try {
|
|
6542
|
+
for (const item of staged) {
|
|
6543
|
+
const entry = remoteLock.hooks[item.name];
|
|
6544
|
+
if (!entry)
|
|
6545
|
+
continue;
|
|
6546
|
+
upsertHookRecord(db, {
|
|
6547
|
+
name: item.name,
|
|
6548
|
+
version: entry.version,
|
|
6549
|
+
sha256: entry.sha256,
|
|
6550
|
+
source_type: entry.source ?? "remote",
|
|
6551
|
+
source_ref: apiUrl,
|
|
6552
|
+
last_verified_at: new Date().toISOString()
|
|
6553
|
+
});
|
|
6554
|
+
}
|
|
6555
|
+
db.exec("COMMIT");
|
|
6556
|
+
} catch (err) {
|
|
6557
|
+
db.exec("ROLLBACK");
|
|
6558
|
+
throw err;
|
|
6559
|
+
}
|
|
6217
6560
|
}
|
|
6218
6561
|
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6219
6562
|
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
@@ -6221,10 +6564,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6221
6564
|
if (!entry) {
|
|
6222
6565
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
6223
6566
|
}
|
|
6224
|
-
|
|
6225
|
-
|
|
6567
|
+
const isLatest = entry.version === version;
|
|
6568
|
+
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
6569
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
6226
6570
|
}
|
|
6227
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}
|
|
6571
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
6228
6572
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6229
6573
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
6230
6574
|
}
|
|
@@ -6236,24 +6580,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6236
6580
|
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
6237
6581
|
}
|
|
6238
6582
|
const actualSha = sha256OfText(artifact.script);
|
|
6239
|
-
|
|
6240
|
-
|
|
6583
|
+
let expectedSha = entry.sha256;
|
|
6584
|
+
if (!isLatest) {
|
|
6585
|
+
const headerSha = artifact._headerSha;
|
|
6586
|
+
if (!headerSha) {
|
|
6587
|
+
throw new Error(`registry did not return a sha256 header for '${name}@${version}' (older pin); refusing to trust an unverified artifact`);
|
|
6588
|
+
}
|
|
6589
|
+
expectedSha = headerSha;
|
|
6590
|
+
}
|
|
6591
|
+
if (actualSha !== expectedSha) {
|
|
6592
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': expected ${expectedSha}, artifact has ${actualSha}`);
|
|
6241
6593
|
}
|
|
6242
6594
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6243
|
-
const scriptRel =
|
|
6244
|
-
`) ? "script.ts" : artifact.manifest.script;
|
|
6595
|
+
const scriptRel = scriptRelFor(manifest);
|
|
6245
6596
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6246
6597
|
const scriptPath = resolveScriptPath(name);
|
|
6247
6598
|
const actual = await sha256File(scriptPath);
|
|
6248
|
-
if (actual !==
|
|
6249
|
-
throw new Error(`sha256 mismatch after write for '${name}@${version}':
|
|
6599
|
+
if (actual !== expectedSha) {
|
|
6600
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
|
|
6250
6601
|
}
|
|
6251
6602
|
const db = getDb();
|
|
6252
|
-
setPinnedHook(name, { version, sha256:
|
|
6603
|
+
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
6253
6604
|
upsertHookRecord(db, {
|
|
6254
6605
|
name,
|
|
6255
6606
|
version,
|
|
6256
|
-
sha256:
|
|
6607
|
+
sha256: expectedSha,
|
|
6257
6608
|
source_type: entry.source ?? "remote",
|
|
6258
6609
|
source_ref: apiUrl,
|
|
6259
6610
|
last_verified_at: new Date().toISOString()
|
|
@@ -6261,7 +6612,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6261
6612
|
return {
|
|
6262
6613
|
name,
|
|
6263
6614
|
version,
|
|
6264
|
-
sha256:
|
|
6615
|
+
sha256: expectedSha,
|
|
6265
6616
|
source: entry.source ?? "remote",
|
|
6266
6617
|
source_ref: apiUrl,
|
|
6267
6618
|
artifact,
|
|
@@ -6286,7 +6637,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
6286
6637
|
timestamp TEXT NOT NULL,
|
|
6287
6638
|
session_id TEXT NOT NULL,
|
|
6288
6639
|
hook_name TEXT NOT NULL,
|
|
6289
|
-
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
|
|
6640
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
|
|
6290
6641
|
tool_name TEXT,
|
|
6291
6642
|
tool_input TEXT,
|
|
6292
6643
|
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
@@ -6319,7 +6670,10 @@ var init_pg_migrations = __esm(() => {
|
|
|
6319
6670
|
)`,
|
|
6320
6671
|
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
6321
6672
|
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
6322
|
-
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))
|
|
6673
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
|
|
6674
|
+
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
6675
|
+
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
6676
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
|
|
6323
6677
|
];
|
|
6324
6678
|
});
|
|
6325
6679
|
|
|
@@ -6334,7 +6688,34 @@ function normalizeParams(params) {
|
|
|
6334
6688
|
return flat.map((value) => value === undefined ? null : value);
|
|
6335
6689
|
}
|
|
6336
6690
|
function sslConfigFor(connectionString) {
|
|
6337
|
-
|
|
6691
|
+
const wantsSsl = wantsSslFor(connectionString);
|
|
6692
|
+
if (!wantsSsl)
|
|
6693
|
+
return;
|
|
6694
|
+
const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
|
|
6695
|
+
if (insecure && false) {}
|
|
6696
|
+
return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
|
|
6697
|
+
}
|
|
6698
|
+
function wantsSslFor(connectionString) {
|
|
6699
|
+
let raw;
|
|
6700
|
+
try {
|
|
6701
|
+
raw = new URL(connectionString).search.replace(/^\?/, "");
|
|
6702
|
+
} catch {
|
|
6703
|
+
raw = connectionString;
|
|
6704
|
+
}
|
|
6705
|
+
try {
|
|
6706
|
+
raw = decodeURIComponent(raw);
|
|
6707
|
+
} catch {}
|
|
6708
|
+
const paramValue = (name) => {
|
|
6709
|
+
const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
|
|
6710
|
+
return match ? match[1].toLowerCase() : undefined;
|
|
6711
|
+
};
|
|
6712
|
+
const sslmode = paramValue("sslmode");
|
|
6713
|
+
if (sslmode !== undefined)
|
|
6714
|
+
return sslmode !== "disable" && sslmode !== "allow";
|
|
6715
|
+
const ssl = paramValue("ssl");
|
|
6716
|
+
if (ssl !== undefined)
|
|
6717
|
+
return ssl !== "false" && ssl !== "0" && ssl !== "no";
|
|
6718
|
+
return false;
|
|
6338
6719
|
}
|
|
6339
6720
|
|
|
6340
6721
|
class PgAdapterAsync {
|
|
@@ -6496,9 +6877,15 @@ async function pushTable(db, remote, table) {
|
|
|
6496
6877
|
result.rowsRead = rows.length;
|
|
6497
6878
|
if (rows.length === 0)
|
|
6498
6879
|
return result;
|
|
6880
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
6881
|
+
...row,
|
|
6882
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
6883
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
6884
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
6885
|
+
})) : rows;
|
|
6499
6886
|
const remoteColumns = await getRemoteColumns(remote, table);
|
|
6500
|
-
const columns = filterRemoteColumns(remoteColumns, Object.keys(
|
|
6501
|
-
result.rowsWritten = await upsertPg(remote, table, columns,
|
|
6887
|
+
const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
|
|
6888
|
+
result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
|
|
6502
6889
|
} catch (error) {
|
|
6503
6890
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6504
6891
|
}
|
|
@@ -6513,8 +6900,14 @@ async function pullTable(remote, db, table) {
|
|
|
6513
6900
|
result.rowsRead = rows.length;
|
|
6514
6901
|
if (rows.length === 0)
|
|
6515
6902
|
return result;
|
|
6516
|
-
const
|
|
6517
|
-
|
|
6903
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
6904
|
+
...row,
|
|
6905
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
6906
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
6907
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
6908
|
+
})) : rows;
|
|
6909
|
+
const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
|
|
6910
|
+
result.rowsWritten = upsertSqlite(db, table, columns, projected);
|
|
6518
6911
|
} catch (error) {
|
|
6519
6912
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6520
6913
|
}
|
|
@@ -6637,6 +7030,7 @@ var init_storage_sync = __esm(() => {
|
|
|
6637
7030
|
init_db();
|
|
6638
7031
|
init_pg_migrations();
|
|
6639
7032
|
init_remote_storage();
|
|
7033
|
+
init_redact();
|
|
6640
7034
|
STORAGE_TABLES = [
|
|
6641
7035
|
"hook_events",
|
|
6642
7036
|
"schema_migrations",
|
|
@@ -6735,24 +7129,29 @@ async function buildCatalog() {
|
|
|
6735
7129
|
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
6736
7130
|
if (!scriptPath)
|
|
6737
7131
|
continue;
|
|
7132
|
+
const sha = await sha256File(scriptPath);
|
|
6738
7133
|
byName.set(meta.name, {
|
|
6739
7134
|
name: meta.name,
|
|
6740
7135
|
version: meta.version,
|
|
6741
|
-
sha256:
|
|
7136
|
+
sha256: sha,
|
|
6742
7137
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
6743
7138
|
description: meta.description,
|
|
6744
|
-
source: "bundled"
|
|
7139
|
+
source: "bundled",
|
|
7140
|
+
versions: [meta.version]
|
|
6745
7141
|
});
|
|
6746
7142
|
}
|
|
6747
7143
|
for (const custom of listCustomHooks()) {
|
|
6748
7144
|
const name = shortManifestName(custom.manifest.name);
|
|
7145
|
+
const sha = await sha256File(custom.scriptPath);
|
|
7146
|
+
const existing = byName.get(name);
|
|
6749
7147
|
byName.set(name, {
|
|
6750
7148
|
name,
|
|
6751
7149
|
version: custom.manifest.version,
|
|
6752
|
-
sha256:
|
|
7150
|
+
sha256: sha,
|
|
6753
7151
|
events: custom.manifest.events,
|
|
6754
7152
|
description: custom.manifest.description ?? "Custom hook",
|
|
6755
|
-
source:
|
|
7153
|
+
source: existing ? "custom-overrides-bundled" : "custom",
|
|
7154
|
+
versions: existing ? [...new Set([...existing.versions, custom.manifest.version])] : [custom.manifest.version]
|
|
6756
7155
|
});
|
|
6757
7156
|
}
|
|
6758
7157
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -6770,6 +7169,7 @@ async function artifactFor(name, version) {
|
|
|
6770
7169
|
description: resolved.description,
|
|
6771
7170
|
events: resolved.events,
|
|
6772
7171
|
script: custom ? custom.manifest.script : `src/hook.ts`,
|
|
7172
|
+
script_kind: custom?.manifest.script_kind,
|
|
6773
7173
|
args: custom?.manifest.args,
|
|
6774
7174
|
timeout_ms: custom?.manifest.timeout_ms
|
|
6775
7175
|
};
|
|
@@ -6799,11 +7199,32 @@ function handleServeRequest(req, apiKey) {
|
|
|
6799
7199
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6800
7200
|
}
|
|
6801
7201
|
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6802
|
-
return
|
|
7202
|
+
return buildCatalog().then((catalog) => {
|
|
7203
|
+
const lock = readLock();
|
|
7204
|
+
const byVersion = {};
|
|
7205
|
+
for (const entry of catalog)
|
|
7206
|
+
byVersion[entry.name] = entry.versions;
|
|
7207
|
+
const hooks = {};
|
|
7208
|
+
for (const [name, pin] of Object.entries(lock.hooks)) {
|
|
7209
|
+
hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
|
|
7210
|
+
}
|
|
7211
|
+
return json({ hooks });
|
|
7212
|
+
});
|
|
6803
7213
|
}
|
|
6804
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(
|
|
7214
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
|
|
6805
7215
|
if (artifactMatch && req.method === "GET") {
|
|
6806
|
-
const [,
|
|
7216
|
+
const [, rawName, rawVersion] = artifactMatch;
|
|
7217
|
+
let name;
|
|
7218
|
+
let version;
|
|
7219
|
+
try {
|
|
7220
|
+
name = decodeURIComponent(rawName);
|
|
7221
|
+
version = decodeURIComponent(rawVersion);
|
|
7222
|
+
} catch {
|
|
7223
|
+
return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
|
|
7224
|
+
}
|
|
7225
|
+
if (!SEMVER_PATTERN.test(version)) {
|
|
7226
|
+
return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
|
|
7227
|
+
}
|
|
6807
7228
|
return artifactFor(name, version).then(async (artifact) => {
|
|
6808
7229
|
if (!artifact)
|
|
6809
7230
|
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
@@ -6834,7 +7255,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
6834
7255
|
function startServeServer(options) {
|
|
6835
7256
|
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6836
7257
|
const host = options.host ?? SERVE_HOST;
|
|
6837
|
-
const apiKey = resolveApiKey(
|
|
7258
|
+
const apiKey = resolveApiKey();
|
|
6838
7259
|
const server = Bun.serve({
|
|
6839
7260
|
hostname: host,
|
|
6840
7261
|
port,
|
|
@@ -6852,6 +7273,7 @@ var init_serve = __esm(() => {
|
|
|
6852
7273
|
init_store();
|
|
6853
7274
|
init_resolve();
|
|
6854
7275
|
init_config();
|
|
7276
|
+
init_semver();
|
|
6855
7277
|
if (false) {}
|
|
6856
7278
|
});
|
|
6857
7279
|
|
|
@@ -6941,7 +7363,9 @@ var exports_server = {};
|
|
|
6941
7363
|
__export(exports_server, {
|
|
6942
7364
|
startStdioServer: () => startStdioServer,
|
|
6943
7365
|
startSSEServer: () => startSSEServer,
|
|
7366
|
+
sseAuthToken: () => sseAuthToken,
|
|
6944
7367
|
createHooksServer: () => createHooksServer,
|
|
7368
|
+
MCP_SSE_HOST: () => MCP_SSE_HOST,
|
|
6945
7369
|
MCP_PORT: () => MCP_PORT
|
|
6946
7370
|
});
|
|
6947
7371
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -6949,14 +7373,14 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
6949
7373
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6950
7374
|
import { z as z2 } from "zod";
|
|
6951
7375
|
import { createServer } from "http";
|
|
6952
|
-
import { existsSync as
|
|
6953
|
-
import { join as join11, dirname as
|
|
7376
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
7377
|
+
import { join as join11, dirname as dirname6 } from "path";
|
|
6954
7378
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6955
7379
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
6956
7380
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
6957
7381
|
const resolved = resolveHook2(name);
|
|
6958
7382
|
const hookScript = resolveScriptPath2(name);
|
|
6959
|
-
if (!resolved || !hookScript || !
|
|
7383
|
+
if (!resolved || !hookScript || !existsSync12(hookScript)) {
|
|
6960
7384
|
throw new Error(`Hook '${name}' not found`);
|
|
6961
7385
|
}
|
|
6962
7386
|
const content = readFileSync9(hookScript);
|
|
@@ -6969,7 +7393,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
6969
7393
|
const started = Date.now();
|
|
6970
7394
|
let ran;
|
|
6971
7395
|
try {
|
|
6972
|
-
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin,
|
|
7396
|
+
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, timeout: timeoutMs });
|
|
6973
7397
|
} catch (err) {
|
|
6974
7398
|
try {
|
|
6975
7399
|
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
@@ -7075,6 +7499,9 @@ function compactEvent(row) {
|
|
|
7075
7499
|
tool_input_preview: row.tool_input ? truncateText(row.tool_input, 180) : undefined
|
|
7076
7500
|
};
|
|
7077
7501
|
}
|
|
7502
|
+
function projectLogRows(rows) {
|
|
7503
|
+
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
7504
|
+
}
|
|
7078
7505
|
function createHooksServer() {
|
|
7079
7506
|
const server = new McpServer({
|
|
7080
7507
|
name: "@hasna/hooks",
|
|
@@ -7161,7 +7588,7 @@ function createHooksServer() {
|
|
|
7161
7588
|
const settingsPath = getSettingsPath(scope);
|
|
7162
7589
|
const issues = [];
|
|
7163
7590
|
const healthy = [];
|
|
7164
|
-
const settingsExist =
|
|
7591
|
+
const settingsExist = existsSync12(settingsPath);
|
|
7165
7592
|
if (!settingsExist) {
|
|
7166
7593
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
7167
7594
|
}
|
|
@@ -7171,7 +7598,7 @@ function createHooksServer() {
|
|
|
7171
7598
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
7172
7599
|
let hookHealthy = true;
|
|
7173
7600
|
if (custom) {
|
|
7174
|
-
if (!
|
|
7601
|
+
if (!existsSync12(custom.scriptPath)) {
|
|
7175
7602
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
7176
7603
|
hookHealthy = false;
|
|
7177
7604
|
}
|
|
@@ -7181,7 +7608,7 @@ function createHooksServer() {
|
|
|
7181
7608
|
continue;
|
|
7182
7609
|
}
|
|
7183
7610
|
const hookDir = getHookPath(name);
|
|
7184
|
-
if (!
|
|
7611
|
+
if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
|
|
7185
7612
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
7186
7613
|
hookHealthy = false;
|
|
7187
7614
|
}
|
|
@@ -7220,7 +7647,7 @@ function createHooksServer() {
|
|
|
7220
7647
|
const hookPath = getHookPath(name);
|
|
7221
7648
|
const readmePath = join11(hookPath, "README.md");
|
|
7222
7649
|
let readme = "";
|
|
7223
|
-
if (
|
|
7650
|
+
if (existsSync12(readmePath)) {
|
|
7224
7651
|
readme = readFileSync9(readmePath, "utf-8");
|
|
7225
7652
|
}
|
|
7226
7653
|
if (verbose) {
|
|
@@ -7404,7 +7831,7 @@ function createHooksServer() {
|
|
|
7404
7831
|
const ctx = {
|
|
7405
7832
|
scope,
|
|
7406
7833
|
settings_path: settingsPath,
|
|
7407
|
-
settings_exists:
|
|
7834
|
+
settings_exists: existsSync12(settingsPath),
|
|
7408
7835
|
registered_hooks: hooks,
|
|
7409
7836
|
hook_count: hooks.length,
|
|
7410
7837
|
healthy,
|
|
@@ -7456,7 +7883,13 @@ function createHooksServer() {
|
|
|
7456
7883
|
} catch (err) {
|
|
7457
7884
|
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7458
7885
|
if (err instanceof HookTimeoutError2) {
|
|
7459
|
-
return {
|
|
7886
|
+
return {
|
|
7887
|
+
name,
|
|
7888
|
+
decision: "block",
|
|
7889
|
+
timedOut: true,
|
|
7890
|
+
timeout_ms: effectiveTimeout,
|
|
7891
|
+
error: `hook timed out after ${effectiveTimeout}ms \u2014 preview blocked (never auto-approved)`
|
|
7892
|
+
};
|
|
7460
7893
|
}
|
|
7461
7894
|
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
7462
7895
|
}
|
|
@@ -7482,15 +7915,25 @@ function createHooksServer() {
|
|
|
7482
7915
|
hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
|
|
7483
7916
|
scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
|
|
7484
7917
|
}, async ({ agent_type, name, hooks, scope }) => {
|
|
7918
|
+
if (agent_type !== "claude" && agent_type !== "gemini" && agent_type !== "custom") {
|
|
7919
|
+
return {
|
|
7920
|
+
content: [{
|
|
7921
|
+
type: "text",
|
|
7922
|
+
text: JSON.stringify({ error: `unsupported agent_type '${String(agent_type)}' (supported: claude, gemini, custom)` })
|
|
7923
|
+
}],
|
|
7924
|
+
isError: true
|
|
7925
|
+
};
|
|
7926
|
+
}
|
|
7485
7927
|
const profile = createProfile({ agent_type, name });
|
|
7928
|
+
const target = agent_type === "gemini" ? "gemini" : "claude";
|
|
7486
7929
|
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
7487
|
-
const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id }));
|
|
7930
|
+
const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id, target }));
|
|
7488
7931
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
7489
7932
|
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
7490
7933
|
return {
|
|
7491
7934
|
content: [{
|
|
7492
7935
|
type: "text",
|
|
7493
|
-
text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
|
|
7936
|
+
text: JSON.stringify({ profile, installed, failed, scope, target, run_with: `hooks run <name> --profile ${profile.agent_id}` })
|
|
7494
7937
|
}]
|
|
7495
7938
|
};
|
|
7496
7939
|
});
|
|
@@ -7534,7 +7977,7 @@ function createHooksServer() {
|
|
|
7534
7977
|
const settingsPath = getSettingsPath(scope);
|
|
7535
7978
|
let settings = {};
|
|
7536
7979
|
try {
|
|
7537
|
-
if (
|
|
7980
|
+
if (existsSync12(settingsPath))
|
|
7538
7981
|
settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
7539
7982
|
} catch {}
|
|
7540
7983
|
if (!settings.hooks)
|
|
@@ -7544,8 +7987,8 @@ function createHooksServer() {
|
|
|
7544
7987
|
disabled.push(name);
|
|
7545
7988
|
settings.hooks.__disabled = disabled;
|
|
7546
7989
|
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
7547
|
-
const { dirname:
|
|
7548
|
-
mkdirSync8(
|
|
7990
|
+
const { dirname: dirname7 } = await import("path");
|
|
7991
|
+
mkdirSync8(dirname7(settingsPath), { recursive: true });
|
|
7549
7992
|
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
7550
7993
|
`);
|
|
7551
7994
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -7557,7 +8000,7 @@ function createHooksServer() {
|
|
|
7557
8000
|
const settingsPath = getSettingsPath(scope);
|
|
7558
8001
|
let settings = {};
|
|
7559
8002
|
try {
|
|
7560
|
-
if (
|
|
8003
|
+
if (existsSync12(settingsPath))
|
|
7561
8004
|
settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
7562
8005
|
} catch {}
|
|
7563
8006
|
if (settings.hooks?.__disabled) {
|
|
@@ -7642,12 +8085,13 @@ function createHooksServer() {
|
|
|
7642
8085
|
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
7643
8086
|
params.push(maxRows);
|
|
7644
8087
|
const rows = db.query(sql).all(...params);
|
|
8088
|
+
const projected = projectLogRows(rows);
|
|
7645
8089
|
return {
|
|
7646
8090
|
content: [{
|
|
7647
8091
|
type: "text",
|
|
7648
8092
|
text: JSON.stringify({
|
|
7649
|
-
events: compact ?
|
|
7650
|
-
count:
|
|
8093
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
8094
|
+
count: projected.length,
|
|
7651
8095
|
compact,
|
|
7652
8096
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7653
8097
|
})
|
|
@@ -7662,12 +8106,13 @@ function createHooksServer() {
|
|
|
7662
8106
|
const db = getDb2();
|
|
7663
8107
|
const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
|
|
7664
8108
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
|
|
8109
|
+
const projected = projectLogRows(rows);
|
|
7665
8110
|
return {
|
|
7666
8111
|
content: [{
|
|
7667
8112
|
type: "text",
|
|
7668
8113
|
text: JSON.stringify({
|
|
7669
|
-
events: compact ?
|
|
7670
|
-
count:
|
|
8114
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
8115
|
+
count: projected.length,
|
|
7671
8116
|
compact,
|
|
7672
8117
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7673
8118
|
})
|
|
@@ -7692,12 +8137,13 @@ function createHooksServer() {
|
|
|
7692
8137
|
}
|
|
7693
8138
|
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
7694
8139
|
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
|
|
8140
|
+
const projected = projectLogRows(rows);
|
|
7695
8141
|
return {
|
|
7696
8142
|
content: [{
|
|
7697
8143
|
type: "text",
|
|
7698
8144
|
text: JSON.stringify({
|
|
7699
|
-
events: compact ?
|
|
7700
|
-
count:
|
|
8145
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
8146
|
+
count: projected.length,
|
|
7701
8147
|
compact,
|
|
7702
8148
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7703
8149
|
})
|
|
@@ -7825,11 +8271,48 @@ function createHooksServer() {
|
|
|
7825
8271
|
});
|
|
7826
8272
|
return server;
|
|
7827
8273
|
}
|
|
7828
|
-
|
|
8274
|
+
function isLoopbackHost(host) {
|
|
8275
|
+
return host === "" || host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
8276
|
+
}
|
|
8277
|
+
function sseAuthToken(explicit) {
|
|
8278
|
+
if (explicit && explicit.length > 0)
|
|
8279
|
+
return explicit;
|
|
8280
|
+
for (const name of MCP_SSE_AUTH_ENV) {
|
|
8281
|
+
const value = process.env[name];
|
|
8282
|
+
if (value && value.length > 0)
|
|
8283
|
+
return value;
|
|
8284
|
+
}
|
|
8285
|
+
return;
|
|
8286
|
+
}
|
|
8287
|
+
async function startSSEServer(options = {}) {
|
|
8288
|
+
const port = options.port ?? MCP_PORT;
|
|
8289
|
+
const host = options.host ?? MCP_SSE_HOST;
|
|
8290
|
+
const authToken = sseAuthToken(options.authToken);
|
|
8291
|
+
const loopback = isLoopbackHost(host);
|
|
8292
|
+
if (!loopback && !authToken) {
|
|
8293
|
+
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.`);
|
|
8294
|
+
}
|
|
7829
8295
|
const server = createHooksServer();
|
|
7830
8296
|
const transports = new Map;
|
|
8297
|
+
function authorized2(headers) {
|
|
8298
|
+
if (!authToken)
|
|
8299
|
+
return false;
|
|
8300
|
+
const rawAuth = headers["authorization"];
|
|
8301
|
+
const header = (Array.isArray(rawAuth) ? rawAuth[0] : rawAuth) ?? "";
|
|
8302
|
+
if (header.startsWith("Bearer "))
|
|
8303
|
+
return secureEqual(header.slice("Bearer ".length), authToken);
|
|
8304
|
+
const rawKey = headers["x-api-key"];
|
|
8305
|
+
return secureEqual((Array.isArray(rawKey) ? rawKey[0] : rawKey) ?? "", authToken);
|
|
8306
|
+
}
|
|
7831
8307
|
const httpServer = createServer(async (req, res) => {
|
|
7832
8308
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
8309
|
+
if (url.pathname === "/sse" || url.pathname === "/messages") {
|
|
8310
|
+
if (authToken && !authorized2(req.headers)) {
|
|
8311
|
+
res.writeHead(401);
|
|
8312
|
+
res.end("unauthorized: valid MCP token required");
|
|
8313
|
+
return;
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
7833
8316
|
if (url.pathname === "/sse") {
|
|
7834
8317
|
const transport = new SSEServerTransport("/messages", res);
|
|
7835
8318
|
transports.set(transport.sessionId, transport);
|
|
@@ -7852,9 +8335,9 @@ async function startSSEServer(port = MCP_PORT) {
|
|
|
7852
8335
|
res.end(JSON.stringify({ name: "@hasna/hooks", version: pkg.version, transport: "sse", port }));
|
|
7853
8336
|
}
|
|
7854
8337
|
});
|
|
7855
|
-
httpServer.listen(port, () => {
|
|
7856
|
-
console.error(`@hasna/hooks MCP server running on http
|
|
7857
|
-
console.error(`SSE endpoint: http
|
|
8338
|
+
httpServer.listen(port, host, () => {
|
|
8339
|
+
console.error(`@hasna/hooks MCP server running on http://${host}:${port}`);
|
|
8340
|
+
console.error(`SSE endpoint: http://${host}:${port}/sse`);
|
|
7858
8341
|
});
|
|
7859
8342
|
}
|
|
7860
8343
|
async function startStdioServer() {
|
|
@@ -7868,7 +8351,7 @@ async function startStdioServer() {
|
|
|
7868
8351
|
process.exit(1);
|
|
7869
8352
|
}
|
|
7870
8353
|
}
|
|
7871
|
-
var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents;
|
|
8354
|
+
var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents, MCP_SSE_HOST = "127.0.0.1", MCP_SSE_AUTH_ENV;
|
|
7872
8355
|
var init_server = __esm(() => {
|
|
7873
8356
|
init_registry();
|
|
7874
8357
|
init_installer();
|
|
@@ -7876,19 +8359,21 @@ var init_server = __esm(() => {
|
|
|
7876
8359
|
init_manifest();
|
|
7877
8360
|
init_resolve();
|
|
7878
8361
|
init_store();
|
|
8362
|
+
init_redact();
|
|
7879
8363
|
init_storage();
|
|
7880
|
-
__dirname3 =
|
|
8364
|
+
__dirname3 = dirname6(fileURLToPath3(import.meta.url));
|
|
7881
8365
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
7882
8366
|
try {
|
|
7883
8367
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
7884
8368
|
const p = join11(__dirname3, rel);
|
|
7885
|
-
if (
|
|
8369
|
+
if (existsSync12(p)) {
|
|
7886
8370
|
pkg = JSON.parse(readFileSync9(p, "utf-8"));
|
|
7887
8371
|
break;
|
|
7888
8372
|
}
|
|
7889
8373
|
}
|
|
7890
8374
|
} catch {}
|
|
7891
8375
|
_hooksAgents = new Map;
|
|
8376
|
+
MCP_SSE_AUTH_ENV = ["HASNA_HOOKS_MCP_TOKEN", "HOOKS_MCP_TOKEN"];
|
|
7892
8377
|
});
|
|
7893
8378
|
|
|
7894
8379
|
// src/mcp/http.ts
|
|
@@ -7950,7 +8435,7 @@ function startMcpHttpServer(options) {
|
|
|
7950
8435
|
var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
|
|
7951
8436
|
var init_http = () => {};
|
|
7952
8437
|
|
|
7953
|
-
//
|
|
8438
|
+
// ../events/dist/commander.js
|
|
7954
8439
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
7955
8440
|
import { Buffer as Buffer2 } from "buffer";
|
|
7956
8441
|
import { existsSync } from "fs";
|
|
@@ -9153,8 +9638,8 @@ var {
|
|
|
9153
9638
|
|
|
9154
9639
|
// src/cli/index.tsx
|
|
9155
9640
|
import chalk2 from "chalk";
|
|
9156
|
-
import { existsSync as
|
|
9157
|
-
import { join as join12, dirname as
|
|
9641
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
|
|
9642
|
+
import { join as join12, dirname as dirname7 } from "path";
|
|
9158
9643
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
9159
9644
|
|
|
9160
9645
|
// src/cli/components/App.tsx
|
|
@@ -10329,13 +10814,15 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
10329
10814
|
// src/cli/index.tsx
|
|
10330
10815
|
init_registry();
|
|
10331
10816
|
init_installer();
|
|
10817
|
+
init_redact();
|
|
10332
10818
|
init_profiles();
|
|
10333
10819
|
init_manifest();
|
|
10334
10820
|
init_resolve();
|
|
10335
10821
|
init_store();
|
|
10822
|
+
init_semver();
|
|
10336
10823
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
10337
|
-
var __dirname4 =
|
|
10338
|
-
var pkgPath =
|
|
10824
|
+
var __dirname4 = dirname7(fileURLToPath4(import.meta.url));
|
|
10825
|
+
var pkgPath = existsSync13(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
|
|
10339
10826
|
var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
10340
10827
|
var program2 = new Command;
|
|
10341
10828
|
function resolveScope(options) {
|
|
@@ -10364,6 +10851,9 @@ function truncateText2(value, max = 96) {
|
|
|
10364
10851
|
return text;
|
|
10365
10852
|
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
10366
10853
|
}
|
|
10854
|
+
function projectLogRows2(rows) {
|
|
10855
|
+
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
10856
|
+
}
|
|
10367
10857
|
function readToken(tokenFile) {
|
|
10368
10858
|
if (tokenFile) {
|
|
10369
10859
|
try {
|
|
@@ -10500,7 +10990,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
10500
10990
|
process.exit(1);
|
|
10501
10991
|
}
|
|
10502
10992
|
const hookScript = resolveScriptPath2(hook);
|
|
10503
|
-
if (!hookScript || !
|
|
10993
|
+
if (!hookScript || !existsSync13(hookScript)) {
|
|
10504
10994
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
10505
10995
|
process.exit(1);
|
|
10506
10996
|
}
|
|
@@ -10625,6 +11115,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10625
11115
|
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
10626
11116
|
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
10627
11117
|
}
|
|
11118
|
+
process.exitCode = 1;
|
|
10628
11119
|
return;
|
|
10629
11120
|
}
|
|
10630
11121
|
toInstall = getHooksByCategory(category).map((h) => h.name);
|
|
@@ -10657,7 +11148,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10657
11148
|
const version = arg.slice(at + 1);
|
|
10658
11149
|
if (!HOOK_NAME_RE2.test(name))
|
|
10659
11150
|
return null;
|
|
10660
|
-
if (
|
|
11151
|
+
if (!SEMVER_PATTERN.test(version))
|
|
10661
11152
|
return null;
|
|
10662
11153
|
return { name, version };
|
|
10663
11154
|
}
|
|
@@ -10791,7 +11282,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10791
11282
|
target,
|
|
10792
11283
|
applied: results.some((r) => r.applied)
|
|
10793
11284
|
}));
|
|
10794
|
-
if (results.
|
|
11285
|
+
if (results.some((r) => !r.success)) {
|
|
10795
11286
|
process.exitCode = 1;
|
|
10796
11287
|
}
|
|
10797
11288
|
return;
|
|
@@ -10829,6 +11320,11 @@ Installing hooks (${scope}, ${target})...
|
|
|
10829
11320
|
process.exitCode = 1;
|
|
10830
11321
|
return;
|
|
10831
11322
|
}
|
|
11323
|
+
if (successCount < results.length) {
|
|
11324
|
+
console.log(chalk2.red(`
|
|
11325
|
+
\u2717 ${results.length - successCount} of ${results.length} hook(s) failed to install.`));
|
|
11326
|
+
process.exitCode = 1;
|
|
11327
|
+
}
|
|
10832
11328
|
console.log(chalk2.dim(`
|
|
10833
11329
|
Registered in ${settingsFile}`));
|
|
10834
11330
|
});
|
|
@@ -11024,6 +11520,7 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
|
|
|
11024
11520
|
} else {
|
|
11025
11521
|
console.log(chalk2.red(`Hook '${hook}' not found${hint}`));
|
|
11026
11522
|
}
|
|
11523
|
+
process.exitCode = 1;
|
|
11027
11524
|
return;
|
|
11028
11525
|
}
|
|
11029
11526
|
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
@@ -11065,17 +11562,24 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11065
11562
|
const settingsPath = getSettingsPath(scope);
|
|
11066
11563
|
const issues = [];
|
|
11067
11564
|
const healthy = [];
|
|
11068
|
-
const settingsExist =
|
|
11565
|
+
const settingsExist = existsSync13(settingsPath);
|
|
11069
11566
|
if (!settingsExist) {
|
|
11070
11567
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
11071
11568
|
}
|
|
11072
11569
|
const registered = getRegisteredHooks(scope);
|
|
11570
|
+
let wiringCount = 0;
|
|
11571
|
+
if (settingsExist) {
|
|
11572
|
+
try {
|
|
11573
|
+
const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
11574
|
+
wiringCount = countSettingsWiring(settings);
|
|
11575
|
+
} catch {}
|
|
11576
|
+
}
|
|
11073
11577
|
for (const name of registered) {
|
|
11074
11578
|
const custom = readCustomManifest(name);
|
|
11075
11579
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
11076
11580
|
let hookHealthy = true;
|
|
11077
11581
|
if (custom) {
|
|
11078
|
-
if (!
|
|
11582
|
+
if (!existsSync13(custom.scriptPath)) {
|
|
11079
11583
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
11080
11584
|
hookHealthy = false;
|
|
11081
11585
|
}
|
|
@@ -11087,7 +11591,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11087
11591
|
}
|
|
11088
11592
|
const hookDir = getHookPath(name);
|
|
11089
11593
|
const hookScript = join12(hookDir, "src", "hook.ts");
|
|
11090
|
-
if (!
|
|
11594
|
+
if (!existsSync13(hookScript)) {
|
|
11091
11595
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
11092
11596
|
hookHealthy = false;
|
|
11093
11597
|
}
|
|
@@ -11107,7 +11611,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11107
11611
|
}
|
|
11108
11612
|
}
|
|
11109
11613
|
if (options.json) {
|
|
11110
|
-
console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
|
|
11614
|
+
console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, wiring_count: wiringCount, scope }));
|
|
11111
11615
|
return;
|
|
11112
11616
|
}
|
|
11113
11617
|
console.log(chalk2.bold(`
|
|
@@ -11116,6 +11620,9 @@ Hook Health Check (${scope})
|
|
|
11116
11620
|
if (registered.length === 0) {
|
|
11117
11621
|
console.log(chalk2.dim(" No hooks registered."));
|
|
11118
11622
|
console.log(chalk2.dim(" Run: hooks install gitguard"));
|
|
11623
|
+
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)`));
|
|
11624
|
+
if (issues.length > 0)
|
|
11625
|
+
process.exitCode = 1;
|
|
11119
11626
|
return;
|
|
11120
11627
|
}
|
|
11121
11628
|
if (healthy.length > 0) {
|
|
@@ -11124,6 +11631,9 @@ Hook Health Check (${scope})
|
|
|
11124
11631
|
console.log(chalk2.green(` ${name}`));
|
|
11125
11632
|
}
|
|
11126
11633
|
}
|
|
11634
|
+
if (registered.length > 0) {
|
|
11635
|
+
console.log(chalk2.dim(` (checked ${registered.length} registered \`hooks run\` entries of ${wiringCount} settings wiring entries; direct-path wiring outside the registered surface is not covered by this check)`));
|
|
11636
|
+
}
|
|
11127
11637
|
if (issues.length > 0) {
|
|
11128
11638
|
console.log();
|
|
11129
11639
|
for (const issue of issues) {
|
|
@@ -11131,9 +11641,11 @@ Hook Health Check (${scope})
|
|
|
11131
11641
|
console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
|
|
11132
11642
|
}
|
|
11133
11643
|
}
|
|
11134
|
-
if (issues.length === 0) {
|
|
11644
|
+
if (issues.length === 0 && registered.length > 0) {
|
|
11135
11645
|
console.log(chalk2.green(`
|
|
11136
|
-
All
|
|
11646
|
+
All ${healthy.length} registered hook(s) healthy`));
|
|
11647
|
+
} else if (issues.some((i) => i.severity === "error")) {
|
|
11648
|
+
process.exitCode = 1;
|
|
11137
11649
|
}
|
|
11138
11650
|
console.log();
|
|
11139
11651
|
});
|
|
@@ -11166,7 +11678,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
11166
11678
|
const version = arg.slice(at + 1);
|
|
11167
11679
|
if (!HOOK_NAME_RE2.test(name))
|
|
11168
11680
|
return null;
|
|
11169
|
-
if (
|
|
11681
|
+
if (!SEMVER_PATTERN.test(version))
|
|
11170
11682
|
return null;
|
|
11171
11683
|
return { name, version };
|
|
11172
11684
|
}
|
|
@@ -11249,12 +11761,13 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
11249
11761
|
} else {
|
|
11250
11762
|
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
11251
11763
|
}
|
|
11764
|
+
process.exitCode = 1;
|
|
11252
11765
|
return;
|
|
11253
11766
|
}
|
|
11254
11767
|
const hookPath = getHookPath(hook);
|
|
11255
11768
|
const readmePath = join12(hookPath, "README.md");
|
|
11256
11769
|
let readme = "";
|
|
11257
|
-
if (
|
|
11770
|
+
if (existsSync13(readmePath)) {
|
|
11258
11771
|
readme = readFileSync10(readmePath, "utf-8");
|
|
11259
11772
|
}
|
|
11260
11773
|
if (options.json) {
|
|
@@ -11503,18 +12016,19 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
|
|
|
11503
12016
|
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
11504
12017
|
params.push(String(limit));
|
|
11505
12018
|
const rows = db.query(sql).all(...params);
|
|
12019
|
+
const projected = projectLogRows2(rows);
|
|
11506
12020
|
if (options.json) {
|
|
11507
|
-
console.log(JSON.stringify(
|
|
12021
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11508
12022
|
return;
|
|
11509
12023
|
}
|
|
11510
|
-
if (
|
|
12024
|
+
if (projected.length === 0) {
|
|
11511
12025
|
console.log(chalk2.dim("No events found."));
|
|
11512
12026
|
return;
|
|
11513
12027
|
}
|
|
11514
12028
|
console.log(chalk2.bold(`
|
|
11515
|
-
Hook Events (${
|
|
12029
|
+
Hook Events (${projected.length})
|
|
11516
12030
|
`));
|
|
11517
|
-
for (const row of
|
|
12031
|
+
for (const row of projected) {
|
|
11518
12032
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11519
12033
|
const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
|
|
11520
12034
|
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
@@ -11529,18 +12043,19 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
|
|
|
11529
12043
|
const limit = parseInt(options.limit) || 50;
|
|
11530
12044
|
const q = `%${text}%`;
|
|
11531
12045
|
const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
|
|
12046
|
+
const projected = projectLogRows2(rows);
|
|
11532
12047
|
if (options.json) {
|
|
11533
|
-
console.log(JSON.stringify(
|
|
12048
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11534
12049
|
return;
|
|
11535
12050
|
}
|
|
11536
|
-
if (
|
|
12051
|
+
if (projected.length === 0) {
|
|
11537
12052
|
console.log(chalk2.dim(`No events matching "${text}".`));
|
|
11538
12053
|
return;
|
|
11539
12054
|
}
|
|
11540
12055
|
console.log(chalk2.bold(`
|
|
11541
|
-
Search results for "${text}" (${
|
|
12056
|
+
Search results for "${text}" (${projected.length})
|
|
11542
12057
|
`));
|
|
11543
|
-
for (const row of
|
|
12058
|
+
for (const row of projected) {
|
|
11544
12059
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11545
12060
|
const snippet = truncateText2(row.tool_input || row.error || "", 80);
|
|
11546
12061
|
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
|
|
@@ -11553,18 +12068,19 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
|
|
|
11553
12068
|
const db = getDb2();
|
|
11554
12069
|
const limit = parseInt(options.n) || 20;
|
|
11555
12070
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
12071
|
+
const projected = projectLogRows2(rows);
|
|
11556
12072
|
if (options.json) {
|
|
11557
|
-
console.log(JSON.stringify(
|
|
12073
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11558
12074
|
return;
|
|
11559
12075
|
}
|
|
11560
|
-
if (
|
|
12076
|
+
if (projected.length === 0) {
|
|
11561
12077
|
console.log(chalk2.dim("No events yet."));
|
|
11562
12078
|
return;
|
|
11563
12079
|
}
|
|
11564
12080
|
console.log(chalk2.bold(`
|
|
11565
|
-
Last ${
|
|
12081
|
+
Last ${projected.length} events
|
|
11566
12082
|
`));
|
|
11567
|
-
for (const row of
|
|
12083
|
+
for (const row of projected) {
|
|
11568
12084
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11569
12085
|
const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
|
|
11570
12086
|
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
@@ -11597,18 +12113,19 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
|
|
|
11597
12113
|
}
|
|
11598
12114
|
const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
|
|
11599
12115
|
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
|
|
12116
|
+
const projected = projectLogRows2(rows);
|
|
11600
12117
|
if (options.json) {
|
|
11601
|
-
console.log(JSON.stringify(
|
|
12118
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11602
12119
|
return;
|
|
11603
12120
|
}
|
|
11604
|
-
if (
|
|
12121
|
+
if (projected.length === 0) {
|
|
11605
12122
|
console.log(chalk2.dim(`No errors in the last ${options.since}.`));
|
|
11606
12123
|
return;
|
|
11607
12124
|
}
|
|
11608
12125
|
console.log(chalk2.bold(`
|
|
11609
|
-
Errors (last ${options.since}, ${
|
|
12126
|
+
Errors (last ${options.since}, ${projected.length} found)
|
|
11610
12127
|
`));
|
|
11611
|
-
for (const row of
|
|
12128
|
+
for (const row of projected) {
|
|
11612
12129
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11613
12130
|
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
|
|
11614
12131
|
}
|
|
@@ -11730,18 +12247,17 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
|
|
|
11730
12247
|
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
11731
12248
|
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
11732
12249
|
});
|
|
11733
|
-
program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").
|
|
12250
|
+
program2.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 HASNA_HOOKS_API_KEY / HOOKS_API_KEY only \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
|
|
11734
12251
|
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
11735
12252
|
startServeServer2({
|
|
11736
12253
|
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
11737
|
-
host: options.host
|
|
11738
|
-
apiKey: options.apiKey
|
|
12254
|
+
host: options.host
|
|
11739
12255
|
});
|
|
11740
12256
|
});
|
|
11741
12257
|
program2.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 API URL is configured)").action(async (options) => {
|
|
11742
12258
|
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
11743
12259
|
try {
|
|
11744
|
-
const plan = options.dryRun ? await
|
|
12260
|
+
const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
|
|
11745
12261
|
const diff = plan.diff;
|
|
11746
12262
|
if (options.json) {
|
|
11747
12263
|
console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
|
|
@@ -11837,7 +12353,7 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
11837
12353
|
await startStdioServer2();
|
|
11838
12354
|
} else if (options.sse) {
|
|
11839
12355
|
const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
11840
|
-
await startSSEServer2(options.port ? parseInt(options.port) : 39427);
|
|
12356
|
+
await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
|
|
11841
12357
|
} else {
|
|
11842
12358
|
const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
11843
12359
|
const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|