@hasna/hooks 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -47,7 +47,7 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/error.js
50
+ // node_modules/commander/lib/error.js
51
51
  var require_error = __commonJS((exports) => {
52
52
  class CommanderError extends Error {
53
53
  constructor(exitCode, code, message) {
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
71
71
  exports.InvalidArgumentError = InvalidArgumentError;
72
72
  });
73
73
 
74
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/argument.js
74
+ // node_modules/commander/lib/argument.js
75
75
  var require_argument = __commonJS((exports) => {
76
76
  var { InvalidArgumentError } = require_error();
77
77
 
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
150
150
  exports.humanReadableArgName = humanReadableArgName;
151
151
  });
152
152
 
153
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/help.js
153
+ // node_modules/commander/lib/help.js
154
154
  var require_help = __commonJS((exports) => {
155
155
  var { humanReadableArgName } = require_argument();
156
156
 
@@ -399,7 +399,7 @@ var require_help = __commonJS((exports) => {
399
399
  exports.Help = Help;
400
400
  });
401
401
 
402
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/option.js
402
+ // node_modules/commander/lib/option.js
403
403
  var require_option = __commonJS((exports) => {
404
404
  var { InvalidArgumentError } = require_error();
405
405
 
@@ -550,7 +550,7 @@ var require_option = __commonJS((exports) => {
550
550
  exports.DualOptions = DualOptions;
551
551
  });
552
552
 
553
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js
553
+ // node_modules/commander/lib/suggestSimilar.js
554
554
  var require_suggestSimilar = __commonJS((exports) => {
555
555
  var maxDistance = 3;
556
556
  function editDistance(a, b) {
@@ -623,7 +623,7 @@ var require_suggestSimilar = __commonJS((exports) => {
623
623
  exports.suggestSimilar = suggestSimilar;
624
624
  });
625
625
 
626
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/command.js
626
+ // node_modules/commander/lib/command.js
627
627
  var require_command = __commonJS((exports) => {
628
628
  var EventEmitter = __require("events").EventEmitter;
629
629
  var childProcess = __require("child_process");
@@ -1866,7 +1866,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1866
1866
  exports.Command = Command;
1867
1867
  });
1868
1868
 
1869
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/index.js
1869
+ // node_modules/commander/index.js
1870
1870
  var require_commander = __commonJS((exports) => {
1871
1871
  var { Argument } = require_argument();
1872
1872
  var { Command } = require_command();
@@ -2419,7 +2419,7 @@ var init_registry = __esm(() => {
2419
2419
  ];
2420
2420
  });
2421
2421
 
2422
- // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
2422
+ // node_modules/cli-spinners/spinners.json
2423
2423
  var require_spinners = __commonJS((exports, module) => {
2424
2424
  module.exports = {
2425
2425
  dots: {
@@ -4045,7 +4045,7 @@ var require_spinners = __commonJS((exports, module) => {
4045
4045
  };
4046
4046
  });
4047
4047
 
4048
- // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
4048
+ // node_modules/cli-spinners/index.js
4049
4049
  var require_cli_spinners = __commonJS((exports, module) => {
4050
4050
  var spinners = Object.assign({}, require_spinners());
4051
4051
  var spinnersList = Object.keys(spinners);
@@ -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(flagKey) {
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 SEMVER_PATTERN;
4129
+ var init_semver = __esm(() => {
4130
+ SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[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 resolveScript(manifest, manifestDir) {
4194
- if (manifest.script.includes(`
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 { path: `script${ext}`, content: manifest.script };
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
- return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline: manifest.script.includes(`
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,13 +4279,15 @@ 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(/^\d+\.\d+\.\d+/, "version must be semver (e.g. 1.2.3)"),
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
4292
  timeout_ms: z.number().int().positive().optional()
4276
4293
  });
@@ -4799,11 +4816,13 @@ __export(exports_store, {
4799
4816
  listHookRecords: () => listHookRecords,
4800
4817
  getPinnedHook: () => getPinnedHook,
4801
4818
  getHookRecord: () => getHookRecord,
4802
- checkScriptHash: () => checkScriptHash
4819
+ checkScriptHash: () => checkScriptHash,
4820
+ LockFileError: () => LockFileError
4803
4821
  });
4804
4822
  import { createHash } from "crypto";
4805
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4823
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
4806
4824
  import { readFile as readFile2 } from "fs/promises";
4825
+ import { randomBytes } from "crypto";
4807
4826
  function sha256Of(input) {
4808
4827
  return createHash("sha256").update(input).digest("hex");
4809
4828
  }
@@ -4846,16 +4865,39 @@ function removeHookRecord(db, name) {
4846
4865
  const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
4847
4866
  return res.changes > 0;
4848
4867
  }
4868
+ function assertLockShape(parsed, path) {
4869
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4870
+ 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.`);
4871
+ }
4872
+ const hooks = parsed.hooks;
4873
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
4874
+ 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.`);
4875
+ }
4876
+ for (const [name, entry] of Object.entries(hooks)) {
4877
+ const e = entry;
4878
+ if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
4879
+ 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.`);
4880
+ }
4881
+ }
4882
+ }
4849
4883
  function readLock() {
4884
+ const path = getLockPath();
4885
+ if (!existsSync7(path))
4886
+ return { hooks: {} };
4887
+ let raw;
4850
4888
  try {
4851
- const path = getLockPath();
4852
- if (!existsSync7(path))
4853
- return { hooks: {} };
4854
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
4855
- return { hooks: parsed.hooks ?? {} };
4889
+ raw = readFileSync4(path, "utf-8");
4856
4890
  } catch {
4857
- return { hooks: {} };
4891
+ throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
4858
4892
  }
4893
+ let parsed;
4894
+ try {
4895
+ parsed = JSON.parse(raw);
4896
+ } catch {
4897
+ 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.`);
4898
+ }
4899
+ assertLockShape(parsed, path);
4900
+ return parsed;
4859
4901
  }
4860
4902
  function writeLock(lock) {
4861
4903
  const path = getLockPath();
@@ -4864,8 +4906,10 @@ function writeLock(lock) {
4864
4906
  for (const name of Object.keys(lock.hooks).sort()) {
4865
4907
  sorted[name] = lock.hooks[name];
4866
4908
  }
4867
- writeFileSync3(path, JSON.stringify({ hooks: sorted }, null, 2) + `
4909
+ const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
4910
+ writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
4868
4911
  `, "utf-8");
4912
+ renameSync(tmp, path);
4869
4913
  return path;
4870
4914
  }
4871
4915
  function setPinnedHook(name, entry) {
@@ -4951,9 +4995,16 @@ function retrustHook(name, scriptPath, version, source) {
4951
4995
  setPinnedHook(name, { version, sha256: actual, source });
4952
4996
  return { ok: true, pinned: true, expected: actual, actual, name };
4953
4997
  }
4998
+ var LockFileError;
4954
4999
  var init_store = __esm(() => {
4955
5000
  init_db();
4956
5001
  init_config();
5002
+ LockFileError = class LockFileError extends Error {
5003
+ constructor(message) {
5004
+ super(message);
5005
+ this.name = "LockFileError";
5006
+ }
5007
+ };
4957
5008
  });
4958
5009
 
4959
5010
  // src/lib/installer.ts
@@ -5006,7 +5057,7 @@ function getGlobalSettingsPathOverride(target) {
5006
5057
  function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
5007
5058
  if (target === "codewith" && codewithConfigPath)
5008
5059
  return codewithConfigPath;
5009
- if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
5060
+ if (target === "codewith" && scope === "global" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
5010
5061
  return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
5011
5062
  }
5012
5063
  const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
@@ -5439,8 +5490,8 @@ function escapeRegExp(value) {
5439
5490
  function codewithHasHookEntryInText(configText, name) {
5440
5491
  return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
5441
5492
  }
5442
- function codewithHasHookEntry(name) {
5443
- const path = getSettingsPath("global", "codewith");
5493
+ function codewithHasHookEntry(name, scope = "global") {
5494
+ const path = getSettingsPath(scope, "codewith");
5444
5495
  if (!existsSync8(path))
5445
5496
  return false;
5446
5497
  try {
@@ -5457,7 +5508,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
5457
5508
  const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
5458
5509
  const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
5459
5510
  const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
5460
- const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
5511
+ const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
5461
5512
  if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
5462
5513
  return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
5463
5514
  }
@@ -5474,7 +5525,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
5474
5525
  }
5475
5526
  const registrationsRemaining = [];
5476
5527
  if (target === "codewith" || target === "all") {
5477
- const codewithPath = getSettingsPath("global", "codewith");
5528
+ const codewithPath = getSettingsPath(scope, "codewith");
5478
5529
  if (existsSync8(codewithPath)) {
5479
5530
  const before = readFileSync5(codewithPath, "utf-8");
5480
5531
  const after = removeCodewithHookEntry(before, shortName);
@@ -5566,6 +5617,22 @@ function safeMatcherTest(pattern, value) {
5566
5617
  return false;
5567
5618
  }
5568
5619
  }
5620
+ function countSettingsWiring(settings) {
5621
+ const hooks = settings.hooks;
5622
+ if (!hooks || typeof hooks !== "object")
5623
+ return 0;
5624
+ let count = 0;
5625
+ for (const eventKey of Object.keys(hooks)) {
5626
+ const entries = hooks[eventKey];
5627
+ if (!Array.isArray(entries))
5628
+ continue;
5629
+ for (const entry of entries) {
5630
+ if (Array.isArray(entry?.hooks))
5631
+ count += entry.hooks.length;
5632
+ }
5633
+ }
5634
+ return count;
5635
+ }
5569
5636
  function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
5570
5637
  const separator = eventSpec.indexOf(":");
5571
5638
  const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
@@ -5587,6 +5654,71 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
5587
5654
  }));
5588
5655
  }
5589
5656
 
5657
+ // src/lib/redact.ts
5658
+ function redactText(value) {
5659
+ let out = value;
5660
+ for (const pattern of SECRET_SHAPES) {
5661
+ out = out.replace(pattern, REDACTED);
5662
+ }
5663
+ return out;
5664
+ }
5665
+ function redactValue2(value) {
5666
+ if (typeof value === "string")
5667
+ return redactText(value);
5668
+ if (Array.isArray(value))
5669
+ return value.map(redactValue2);
5670
+ if (value !== null && typeof value === "object") {
5671
+ const out = {};
5672
+ for (const [key, child] of Object.entries(value)) {
5673
+ out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue2(child);
5674
+ }
5675
+ return out;
5676
+ }
5677
+ return value;
5678
+ }
5679
+ function redactEventPayload(text) {
5680
+ if (text === null || text === undefined)
5681
+ return null;
5682
+ try {
5683
+ const parsed = JSON.parse(text);
5684
+ return redactText(JSON.stringify(redactValue2(parsed)));
5685
+ } catch {
5686
+ return redactText(text);
5687
+ }
5688
+ }
5689
+ function projectEventRowForRead(row) {
5690
+ const out = { ...row };
5691
+ for (const field of ["tool_input", "error", "metadata"]) {
5692
+ const value = out[field];
5693
+ if (typeof value === "string")
5694
+ out[field] = redactEventPayload(value);
5695
+ else if (value === null)
5696
+ out[field] = null;
5697
+ }
5698
+ return out;
5699
+ }
5700
+ var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
5701
+ var init_redact = __esm(() => {
5702
+ 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;
5703
+ SECRET_SHAPES = [
5704
+ /\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
5705
+ /\bsk-[A-Za-z0-9]{16,}/g,
5706
+ /\btpe_[A-Za-z0-9]{8,}/g,
5707
+ /\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
5708
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
5709
+ /gh[pousr]_[A-Za-z0-9]{20,}/g,
5710
+ /xox[baprs]-[A-Za-z0-9-]{20,}/g,
5711
+ /AKIA[0-9A-Z]{16}/g,
5712
+ /ASIA[0-9A-Z]{16}/g,
5713
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
5714
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
5715
+ /-----BEGIN OPENSSH PRIVATE KEY-----/g,
5716
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
5717
+ /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
5718
+ /\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
5719
+ ];
5720
+ });
5721
+
5590
5722
  // src/lib/profiles.ts
5591
5723
  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
5724
  import { join as join8 } from "path";
@@ -5708,6 +5840,9 @@ function writeHookEvent(event) {
5708
5840
  const db = getDb();
5709
5841
  const id = nanoid2();
5710
5842
  const timestamp = event.timestamp ?? new Date().toISOString();
5843
+ const toolInput = redactEventPayload(event.tool_input);
5844
+ const error = redactEventPayload(event.error);
5845
+ const metadata = redactEventPayload(event.metadata);
5711
5846
  db.run(`INSERT INTO hook_events
5712
5847
  (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
5713
5848
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
@@ -5717,12 +5852,12 @@ function writeHookEvent(event) {
5717
5852
  event.hook_name,
5718
5853
  event.event_type,
5719
5854
  event.tool_name ?? null,
5720
- event.tool_input ? event.tool_input.slice(0, 500) : null,
5855
+ toolInput ? toolInput.slice(0, 500) : null,
5721
5856
  event.result ?? null,
5722
- event.error ?? null,
5857
+ error ?? null,
5723
5858
  event.duration_ms ?? null,
5724
5859
  event.project_dir ?? null,
5725
- event.metadata ?? null
5860
+ metadata ?? null
5726
5861
  ]);
5727
5862
  } catch (err) {
5728
5863
  process.stderr.write(`[hooks db-writer] failed to write event: ${err}
@@ -5772,6 +5907,7 @@ function recordHookRun(record) {
5772
5907
  var ALLOWED_EVENT_TYPES;
5773
5908
  var init_db_writer = __esm(() => {
5774
5909
  init_db();
5910
+ init_redact();
5775
5911
  ALLOWED_EVENT_TYPES = new Set([
5776
5912
  "PreToolUse",
5777
5913
  "PostToolUse",
@@ -5784,13 +5920,73 @@ var init_db_writer = __esm(() => {
5784
5920
  ]);
5785
5921
  });
5786
5922
 
5923
+ // src/lib/hook-env.ts
5924
+ function isDeniedEnvName(name) {
5925
+ const upper = name.toUpperCase();
5926
+ if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
5927
+ return true;
5928
+ if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
5929
+ return true;
5930
+ return DENY_CONTAINS.some((token) => upper.includes(token));
5931
+ }
5932
+ function buildHookEnv(source, extra) {
5933
+ const out = {};
5934
+ for (const name of ALLOWLIST) {
5935
+ const value = source[name] ?? extra?.[name];
5936
+ if (value !== undefined && value !== null)
5937
+ out[name] = value;
5938
+ }
5939
+ for (const [from, to] of CONFIG_PROJECTIONS) {
5940
+ const value = source[from] ?? extra?.[from];
5941
+ if (value !== undefined && value !== null)
5942
+ out[to] = value;
5943
+ }
5944
+ const merged = { ...source, ...extra ?? {} };
5945
+ for (const [name, value] of Object.entries(merged)) {
5946
+ if (out[name] !== undefined)
5947
+ continue;
5948
+ if (isDeniedEnvName(name))
5949
+ continue;
5950
+ if (value !== undefined && value !== null)
5951
+ out[name] = value;
5952
+ }
5953
+ return out;
5954
+ }
5955
+ var ALLOWLIST, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, CONFIG_PROJECTIONS;
5956
+ var init_hook_env = __esm(() => {
5957
+ ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
5958
+ DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
5959
+ DENY_PREFIXES = [
5960
+ "HASNA_",
5961
+ "AWS_",
5962
+ "AZURE_",
5963
+ "GCP_",
5964
+ "VAULT_",
5965
+ "GOOGLE_",
5966
+ "OPENAI_",
5967
+ "ANTHROPIC_",
5968
+ "POSTGRES_",
5969
+ "MYSQL_",
5970
+ "REDIS_",
5971
+ "MONGO_",
5972
+ "MEMENTOS_"
5973
+ ];
5974
+ DENY_CONTAINS = ["DATABASE_URL"];
5975
+ CONFIG_PROJECTIONS = [
5976
+ ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
5977
+ ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
5978
+ ["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
5979
+ ["HASNA_HOOKS_CONFIG_PATH", "HOOKS_CONFIG_PATH"]
5980
+ ];
5981
+ });
5982
+
5787
5983
  // src/lib/run.ts
5788
5984
  var exports_run = {};
5789
5985
  __export(exports_run, {
5790
5986
  executeVerifiedScript: () => executeVerifiedScript,
5791
5987
  HookTimeoutError: () => HookTimeoutError
5792
5988
  });
5793
- import { randomBytes } from "crypto";
5989
+ import { randomBytes as randomBytes2 } from "crypto";
5794
5990
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
5795
5991
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
5796
5992
  function extensionOf(scriptPath) {
@@ -5863,12 +6059,12 @@ async function readPipeWithDeadline(stream, deadlineMs) {
5863
6059
  async function executeVerifiedScript(options) {
5864
6060
  const scriptDir = dirname4(options.scriptPath);
5865
6061
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5866
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
6062
+ const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5867
6063
  let proc = null;
5868
6064
  let timedOut = false;
5869
6065
  const timeoutTimer = options.timeout ? setTimeout(() => {
5870
6066
  timedOut = true;
5871
- killGroup(proc);
6067
+ killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
5872
6068
  }, options.timeout) : null;
5873
6069
  try {
5874
6070
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
@@ -5876,7 +6072,7 @@ async function executeVerifiedScript(options) {
5876
6072
  stdin: new Response(options.stdin),
5877
6073
  stdout: "pipe",
5878
6074
  stderr: "pipe",
5879
- env: options.env ?? process.env,
6075
+ env: buildHookEnv(process.env, options.env),
5880
6076
  detached: true
5881
6077
  });
5882
6078
  const exitCode = await proc.exited;
@@ -5887,7 +6083,7 @@ async function executeVerifiedScript(options) {
5887
6083
  readPipeWithDeadline(proc.stdout, 750),
5888
6084
  readPipeWithDeadline(proc.stderr, 750)
5889
6085
  ]);
5890
- killGroup(proc);
6086
+ killGroup(proc, { leaderAlive: false });
5891
6087
  return { stdout: stdoutText, stderr: stderrText, exitCode };
5892
6088
  } finally {
5893
6089
  if (timeoutTimer)
@@ -5897,12 +6093,36 @@ async function executeVerifiedScript(options) {
5897
6093
  } catch {}
5898
6094
  }
5899
6095
  }
5900
- function killGroup(proc) {
6096
+ function killGroup(proc, options) {
5901
6097
  if (!proc)
5902
6098
  return;
5903
- try {
5904
- process.kill(-proc.pid, "SIGKILL");
5905
- } catch {
6099
+ const pgid = proc.pid;
6100
+ for (let pass = 0;pass < 5; pass++) {
6101
+ let members = [];
6102
+ try {
6103
+ const res = Bun.spawnSync(["ps", "-eo", "pid=,pgid="], { stdout: "pipe", stderr: "pipe" });
6104
+ if (res.exitCode === 0) {
6105
+ members = res.stdout.toString().split(`
6106
+ `).map((line) => line.trim().split(/\s+/)).filter((cols) => cols.length === 2 && cols[1] === String(pgid)).map((cols) => Number(cols[0])).filter((pid) => Number.isInteger(pid) && pid > 1).filter((pid) => options.leaderAlive || pid !== proc.pid);
6107
+ }
6108
+ } catch {}
6109
+ if (members.length === 0)
6110
+ break;
6111
+ let killed = 0;
6112
+ for (const pid of members) {
6113
+ try {
6114
+ const probe = Bun.spawnSync(["ps", "-o", "pid=,pgid=", "-p", String(pid)], { stdout: "pipe", stderr: "pipe" });
6115
+ const cols = probe.stdout.toString().trim().split(/\s+/);
6116
+ if (cols.length === 2 && cols[1] === String(pgid)) {
6117
+ process.kill(pid, "SIGKILL");
6118
+ killed++;
6119
+ }
6120
+ } catch {}
6121
+ }
6122
+ if (killed === 0)
6123
+ break;
6124
+ }
6125
+ if (options.leaderAlive) {
5906
6126
  try {
5907
6127
  process.kill(proc.pid, "SIGKILL");
5908
6128
  } catch {}
@@ -5910,6 +6130,7 @@ function killGroup(proc) {
5910
6130
  }
5911
6131
  var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
5912
6132
  var init_run = __esm(() => {
6133
+ init_hook_env();
5913
6134
  HookTimeoutError = class HookTimeoutError extends Error {
5914
6135
  timeoutMs;
5915
6136
  constructor(timeoutMs) {
@@ -6004,19 +6225,20 @@ async function installCustomSource(source) {
6004
6225
  return copyManifestDir(source, source, "local");
6005
6226
  }
6006
6227
  async function installManifestUrl(url) {
6007
- const res = await fetch(url, { redirect: "follow" });
6228
+ const res = await fetch(url, { redirect: "error" });
6008
6229
  if (!res.ok) {
6009
6230
  throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
6010
6231
  }
6011
6232
  const manifest = parseManifest(await res.text());
6012
6233
  let script;
6013
- if (manifest.script.includes(`
6014
- `)) {
6234
+ const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
6235
+ `);
6236
+ if (inline) {
6015
6237
  script = { path: "script.ts", content: manifest.script };
6016
6238
  } else {
6017
6239
  const base = url.substring(0, url.lastIndexOf("/"));
6018
6240
  const scriptUrl = `${base}/${manifest.script}`;
6019
- const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
6241
+ const scriptRes = await fetch(scriptUrl, { redirect: "error" });
6020
6242
  if (!scriptRes.ok) {
6021
6243
  throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
6022
6244
  }
@@ -6040,8 +6262,10 @@ var init_custom_install = __esm(() => {
6040
6262
  var exports_sync = {};
6041
6263
  __export(exports_sync, {
6042
6264
  syncHooks: () => syncHooks,
6265
+ stageSyncArtifacts: () => stageSyncArtifacts,
6043
6266
  planSync: () => planSync,
6044
- fetchPinnedHook: () => fetchPinnedHook
6267
+ fetchPinnedHook: () => fetchPinnedHook,
6268
+ commitSyncArtifacts: () => commitSyncArtifacts
6045
6269
  });
6046
6270
  import { createHash as createHash2 } from "crypto";
6047
6271
  import { readFileSync as readFileSync8 } from "fs";
@@ -6065,7 +6289,7 @@ function collectBundledCatalog() {
6065
6289
  }
6066
6290
  return out;
6067
6291
  }
6068
- async function fetchJson(base, path) {
6292
+ async function fetchJson(base, path, opts) {
6069
6293
  const headers = { accept: "application/json" };
6070
6294
  const apiKey = resolveApiKey();
6071
6295
  if (apiKey)
@@ -6081,7 +6305,11 @@ async function fetchJson(base, path) {
6081
6305
  if (!res.ok) {
6082
6306
  throw new Error(`GET ${path} failed with status ${res.status}`);
6083
6307
  }
6084
- return res.json();
6308
+ const body = await res.json();
6309
+ if (opts?.withHeader) {
6310
+ body[opts.withHeader] = res.headers.get("x-hook-sha256");
6311
+ }
6312
+ return body;
6085
6313
  }
6086
6314
  async function fetchRemoteState(apiUrl) {
6087
6315
  const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
@@ -6094,6 +6322,16 @@ async function fetchRemoteState(apiUrl) {
6094
6322
  }
6095
6323
  return { catalog: catalog.hooks, remoteLock };
6096
6324
  }
6325
+ function classifyRemotePin(pin, remoteVersion, remoteSha) {
6326
+ const pinnedLocally = pin !== undefined && pin.version === remoteVersion && pin.sha256 === remoteSha;
6327
+ if (pinnedLocally)
6328
+ return "unchanged";
6329
+ if (pin !== undefined && pin.pinned === true && pin.version !== remoteVersion)
6330
+ return "unchanged";
6331
+ if (pin !== undefined)
6332
+ return "updated";
6333
+ return "added";
6334
+ }
6097
6335
  function computeDiff(catalog, remoteLock) {
6098
6336
  const local = readLock();
6099
6337
  const diff = { added: [], updated: [], unchanged: [], skipped: [] };
@@ -6101,30 +6339,30 @@ function computeDiff(catalog, remoteLock) {
6101
6339
  if (!entry.name || !entry.version || !entry.sha256)
6102
6340
  continue;
6103
6341
  const pin = local.hooks[entry.name];
6104
- const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
6105
- if (remoteLock && !remotePin) {
6106
- diff.skipped.push(entry.name);
6107
- continue;
6108
- }
6109
- const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
6110
- if (pinnedLocally) {
6111
- diff.unchanged.push(entry.name);
6112
- } else if (pin) {
6113
- diff.updated.push(entry.name);
6342
+ if (remoteLock) {
6343
+ const remotePin = remoteLock.hooks[entry.name];
6344
+ if (!remotePin) {
6345
+ diff.skipped.push(entry.name);
6346
+ continue;
6347
+ }
6348
+ if (remotePin.version !== entry.version) {
6349
+ 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.`);
6350
+ }
6351
+ diff[classifyRemotePin(pin, remotePin.version, remotePin.sha256)].push(entry.name);
6114
6352
  } else {
6115
- diff.added.push(entry.name);
6353
+ diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
6116
6354
  }
6117
6355
  }
6118
6356
  return diff;
6119
6357
  }
6120
- async function planSync() {
6358
+ async function planSync(options = {}) {
6121
6359
  const apiUrl = resolveApiUrl();
6122
6360
  if (apiUrl) {
6123
6361
  const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6124
- return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
6362
+ return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
6125
6363
  }
6126
6364
  const diff = computeDiff(collectBundledCatalog(), null);
6127
- return { apiUrl: null, dryRun: false, diff };
6365
+ return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
6128
6366
  }
6129
6367
  async function syncHooks(options = {}) {
6130
6368
  const apiUrl = resolveApiUrl();
@@ -6132,10 +6370,10 @@ async function syncHooks(options = {}) {
6132
6370
  if (options.dryRun)
6133
6371
  return { ...plan, dryRun: true };
6134
6372
  if (!apiUrl) {
6135
- const db2 = getDb();
6373
+ const db = getDb();
6136
6374
  for (const entry of collectBundledCatalog()) {
6137
6375
  setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
6138
- upsertHookRecord(db2, {
6376
+ upsertHookRecord(db, {
6139
6377
  name: entry.name,
6140
6378
  version: entry.version,
6141
6379
  sha256: entry.sha256,
@@ -6148,13 +6386,17 @@ async function syncHooks(options = {}) {
6148
6386
  }
6149
6387
  const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6150
6388
  const diff = computeDiff(catalog, remoteLock);
6151
- const toFetch = [...diff.added, ...diff.updated];
6152
- const artifacts = new Map;
6153
- for (const name of toFetch) {
6389
+ const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
6390
+ await commitSyncArtifacts(staged, apiUrl, remoteLock);
6391
+ return { apiUrl, dryRun: false, diff };
6392
+ }
6393
+ async function stageSyncArtifacts(apiUrl, names, remoteLock) {
6394
+ const staged = [];
6395
+ for (const name of names) {
6154
6396
  const entry = remoteLock.hooks[name];
6155
6397
  if (!entry)
6156
6398
  continue;
6157
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
6399
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
6158
6400
  if (!artifact.manifest || typeof artifact.script !== "string") {
6159
6401
  throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
6160
6402
  }
@@ -6162,34 +6404,64 @@ async function syncHooks(options = {}) {
6162
6404
  if (actualSha !== entry.sha256) {
6163
6405
  throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
6164
6406
  }
6165
- artifacts.set(name, artifact);
6166
- }
6167
- const db = getDb();
6168
- for (const name of toFetch) {
6169
- const artifact = artifacts.get(name);
6170
- const entry = remoteLock.hooks[name];
6171
- if (!artifact || !entry)
6172
- continue;
6173
6407
  const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
6174
- const scriptRel = artifact.manifest.script.includes(`
6175
- `) ? "script.ts" : artifact.manifest.script;
6176
- writeCustomHook(name, manifest, artifact.script, scriptRel);
6177
- const scriptPath = resolveScriptPath(name);
6178
- const actual = await sha256File(scriptPath);
6179
- if (actual !== entry.sha256) {
6180
- throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
6408
+ if (manifest.version !== entry.version) {
6409
+ throw new Error(`artifact for '${name}@${entry.version}' declares a different version ('${manifest.version}')`);
6181
6410
  }
6182
- setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
6183
- upsertHookRecord(db, {
6411
+ const scriptRel = scriptRelFor(manifest);
6412
+ staged.push({
6184
6413
  name,
6185
6414
  version: entry.version,
6186
6415
  sha256: entry.sha256,
6187
- source_type: entry.source ?? "remote",
6188
- source_ref: apiUrl,
6189
- last_verified_at: new Date().toISOString()
6416
+ source: entry.source ?? "remote",
6417
+ manifest,
6418
+ scriptContent: artifact.script,
6419
+ scriptRel
6190
6420
  });
6191
6421
  }
6192
- return { apiUrl, dryRun: false, diff };
6422
+ return staged;
6423
+ }
6424
+ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
6425
+ const db = getDb();
6426
+ for (const item of staged) {
6427
+ const entry = remoteLock.hooks[item.name];
6428
+ if (!entry)
6429
+ continue;
6430
+ writeCustomHook(item.name, item.manifest, item.scriptContent, item.scriptRel);
6431
+ const scriptPath = resolveScriptPath(item.name);
6432
+ const actual = await sha256File(scriptPath);
6433
+ if (actual !== entry.sha256) {
6434
+ throw new Error(`sha256 mismatch after write for '${item.name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
6435
+ }
6436
+ }
6437
+ const lock = readLock();
6438
+ for (const item of staged) {
6439
+ const entry = remoteLock.hooks[item.name];
6440
+ if (!entry)
6441
+ continue;
6442
+ lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
6443
+ }
6444
+ writeLock(lock);
6445
+ db.exec("BEGIN");
6446
+ try {
6447
+ for (const item of staged) {
6448
+ const entry = remoteLock.hooks[item.name];
6449
+ if (!entry)
6450
+ continue;
6451
+ upsertHookRecord(db, {
6452
+ name: item.name,
6453
+ version: entry.version,
6454
+ sha256: entry.sha256,
6455
+ source_type: entry.source ?? "remote",
6456
+ source_ref: apiUrl,
6457
+ last_verified_at: new Date().toISOString()
6458
+ });
6459
+ }
6460
+ db.exec("COMMIT");
6461
+ } catch (err) {
6462
+ db.exec("ROLLBACK");
6463
+ throw err;
6464
+ }
6193
6465
  }
6194
6466
  async function fetchPinnedHook(name, version, apiUrl) {
6195
6467
  const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
@@ -6197,10 +6469,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
6197
6469
  if (!entry) {
6198
6470
  throw new Error(`Hook '${name}' is not in the remote registry lock`);
6199
6471
  }
6200
- if (entry.version !== version) {
6201
- throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
6472
+ const isLatest = entry.version === version;
6473
+ if (!isLatest && !(entry.versions ?? []).includes(version)) {
6474
+ throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
6202
6475
  }
6203
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
6476
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
6204
6477
  if (!artifact.manifest || typeof artifact.script !== "string") {
6205
6478
  throw new Error(`artifact for '${name}@${version}' is malformed`);
6206
6479
  }
@@ -6212,24 +6485,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
6212
6485
  throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
6213
6486
  }
6214
6487
  const actualSha = sha256OfText(artifact.script);
6215
- if (actualSha !== entry.sha256) {
6216
- throw new Error(`sha256 mismatch for '${name}@${version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
6488
+ let expectedSha = entry.sha256;
6489
+ if (!isLatest) {
6490
+ const headerSha = artifact._headerSha;
6491
+ if (!headerSha) {
6492
+ throw new Error(`registry did not return a sha256 header for '${name}@${version}' (older pin); refusing to trust an unverified artifact`);
6493
+ }
6494
+ expectedSha = headerSha;
6495
+ }
6496
+ if (actualSha !== expectedSha) {
6497
+ throw new Error(`sha256 mismatch for '${name}@${version}': expected ${expectedSha}, artifact has ${actualSha}`);
6217
6498
  }
6218
6499
  const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
6219
- const scriptRel = artifact.manifest.script.includes(`
6220
- `) ? "script.ts" : artifact.manifest.script;
6500
+ const scriptRel = scriptRelFor(manifest);
6221
6501
  writeCustomHook(name, manifest, artifact.script, scriptRel);
6222
6502
  const scriptPath = resolveScriptPath(name);
6223
6503
  const actual = await sha256File(scriptPath);
6224
- if (actual !== entry.sha256) {
6225
- throw new Error(`sha256 mismatch after write for '${name}@${version}': lock says ${entry.sha256}, on disk ${actual}`);
6504
+ if (actual !== expectedSha) {
6505
+ throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
6226
6506
  }
6227
6507
  const db = getDb();
6228
- setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
6508
+ setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
6229
6509
  upsertHookRecord(db, {
6230
6510
  name,
6231
6511
  version,
6232
- sha256: entry.sha256,
6512
+ sha256: expectedSha,
6233
6513
  source_type: entry.source ?? "remote",
6234
6514
  source_ref: apiUrl,
6235
6515
  last_verified_at: new Date().toISOString()
@@ -6237,7 +6517,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
6237
6517
  return {
6238
6518
  name,
6239
6519
  version,
6240
- sha256: entry.sha256,
6520
+ sha256: expectedSha,
6241
6521
  source: entry.source ?? "remote",
6242
6522
  source_ref: apiUrl,
6243
6523
  artifact,
@@ -6262,7 +6542,7 @@ var init_pg_migrations = __esm(() => {
6262
6542
  timestamp TEXT NOT NULL,
6263
6543
  session_id TEXT NOT NULL,
6264
6544
  hook_name TEXT NOT NULL,
6265
- event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
6545
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
6266
6546
  tool_name TEXT,
6267
6547
  tool_input TEXT,
6268
6548
  result TEXT CHECK (result IN ('continue', 'block', NULL)),
@@ -6295,7 +6575,10 @@ var init_pg_migrations = __esm(() => {
6295
6575
  )`,
6296
6576
  `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
6297
6577
  `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
6298
- CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`
6578
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
6579
+ `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
6580
+ `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
6581
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
6299
6582
  ];
6300
6583
  });
6301
6584
 
@@ -6310,7 +6593,34 @@ function normalizeParams(params) {
6310
6593
  return flat.map((value) => value === undefined ? null : value);
6311
6594
  }
6312
6595
  function sslConfigFor(connectionString) {
6313
- return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6596
+ const wantsSsl = wantsSslFor(connectionString);
6597
+ if (!wantsSsl)
6598
+ return;
6599
+ const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
6600
+ if (insecure && false) {}
6601
+ return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
6602
+ }
6603
+ function wantsSslFor(connectionString) {
6604
+ let raw;
6605
+ try {
6606
+ raw = new URL(connectionString).search.replace(/^\?/, "");
6607
+ } catch {
6608
+ raw = connectionString;
6609
+ }
6610
+ try {
6611
+ raw = decodeURIComponent(raw);
6612
+ } catch {}
6613
+ const paramValue = (name) => {
6614
+ const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
6615
+ return match ? match[1].toLowerCase() : undefined;
6616
+ };
6617
+ const sslmode = paramValue("sslmode");
6618
+ if (sslmode !== undefined)
6619
+ return sslmode !== "disable" && sslmode !== "allow";
6620
+ const ssl = paramValue("ssl");
6621
+ if (ssl !== undefined)
6622
+ return ssl !== "false" && ssl !== "0" && ssl !== "no";
6623
+ return false;
6314
6624
  }
6315
6625
 
6316
6626
  class PgAdapterAsync {
@@ -6472,9 +6782,15 @@ async function pushTable(db, remote, table) {
6472
6782
  result.rowsRead = rows.length;
6473
6783
  if (rows.length === 0)
6474
6784
  return result;
6785
+ const projected = table === "hook_events" ? rows.map((row) => ({
6786
+ ...row,
6787
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
6788
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
6789
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
6790
+ })) : rows;
6475
6791
  const remoteColumns = await getRemoteColumns(remote, table);
6476
- const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
6477
- result.rowsWritten = await upsertPg(remote, table, columns, rows, remoteColumns);
6792
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
6793
+ result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
6478
6794
  } catch (error) {
6479
6795
  result.errors.push(error instanceof Error ? error.message : String(error));
6480
6796
  }
@@ -6489,8 +6805,14 @@ async function pullTable(remote, db, table) {
6489
6805
  result.rowsRead = rows.length;
6490
6806
  if (rows.length === 0)
6491
6807
  return result;
6492
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
6493
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
6808
+ const projected = table === "hook_events" ? rows.map((row) => ({
6809
+ ...row,
6810
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
6811
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
6812
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
6813
+ })) : rows;
6814
+ const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
6815
+ result.rowsWritten = upsertSqlite(db, table, columns, projected);
6494
6816
  } catch (error) {
6495
6817
  result.errors.push(error instanceof Error ? error.message : String(error));
6496
6818
  }
@@ -6613,6 +6935,7 @@ var init_storage_sync = __esm(() => {
6613
6935
  init_db();
6614
6936
  init_pg_migrations();
6615
6937
  init_remote_storage();
6938
+ init_redact();
6616
6939
  STORAGE_TABLES = [
6617
6940
  "hook_events",
6618
6941
  "schema_migrations",
@@ -6711,24 +7034,29 @@ async function buildCatalog() {
6711
7034
  const scriptPath = resolveHook(meta.name)?.scriptPath;
6712
7035
  if (!scriptPath)
6713
7036
  continue;
7037
+ const sha = await sha256File(scriptPath);
6714
7038
  byName.set(meta.name, {
6715
7039
  name: meta.name,
6716
7040
  version: meta.version,
6717
- sha256: await sha256File(scriptPath),
7041
+ sha256: sha,
6718
7042
  events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
6719
7043
  description: meta.description,
6720
- source: "bundled"
7044
+ source: "bundled",
7045
+ versions: [meta.version]
6721
7046
  });
6722
7047
  }
6723
7048
  for (const custom of listCustomHooks()) {
6724
7049
  const name = shortManifestName(custom.manifest.name);
7050
+ const sha = await sha256File(custom.scriptPath);
7051
+ const existing = byName.get(name);
6725
7052
  byName.set(name, {
6726
7053
  name,
6727
7054
  version: custom.manifest.version,
6728
- sha256: await sha256File(custom.scriptPath),
7055
+ sha256: sha,
6729
7056
  events: custom.manifest.events,
6730
7057
  description: custom.manifest.description ?? "Custom hook",
6731
- source: byName.has(name) ? "custom-overrides-bundled" : "custom"
7058
+ source: existing ? "custom-overrides-bundled" : "custom",
7059
+ versions: existing ? [...new Set([...existing.versions, custom.manifest.version])] : [custom.manifest.version]
6732
7060
  });
6733
7061
  }
6734
7062
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -6746,6 +7074,7 @@ async function artifactFor(name, version) {
6746
7074
  description: resolved.description,
6747
7075
  events: resolved.events,
6748
7076
  script: custom ? custom.manifest.script : `src/hook.ts`,
7077
+ script_kind: custom?.manifest.script_kind,
6749
7078
  args: custom?.manifest.args,
6750
7079
  timeout_ms: custom?.manifest.timeout_ms
6751
7080
  };
@@ -6775,11 +7104,32 @@ function handleServeRequest(req, apiKey) {
6775
7104
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
6776
7105
  }
6777
7106
  if (url.pathname === "/api/v1/lock" && req.method === "GET") {
6778
- return Promise.resolve(json(readLock()));
7107
+ return buildCatalog().then((catalog) => {
7108
+ const lock = readLock();
7109
+ const byVersion = {};
7110
+ for (const entry of catalog)
7111
+ byVersion[entry.name] = entry.versions;
7112
+ const hooks = {};
7113
+ for (const [name, pin] of Object.entries(lock.hooks)) {
7114
+ hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
7115
+ }
7116
+ return json({ hooks });
7117
+ });
6779
7118
  }
6780
- const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
7119
+ const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
6781
7120
  if (artifactMatch && req.method === "GET") {
6782
- const [, name, version] = artifactMatch;
7121
+ const [, rawName, rawVersion] = artifactMatch;
7122
+ let name;
7123
+ let version;
7124
+ try {
7125
+ name = decodeURIComponent(rawName);
7126
+ version = decodeURIComponent(rawVersion);
7127
+ } catch {
7128
+ return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
7129
+ }
7130
+ if (!SEMVER_PATTERN.test(version)) {
7131
+ return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
7132
+ }
6783
7133
  return artifactFor(name, version).then(async (artifact) => {
6784
7134
  if (!artifact)
6785
7135
  return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
@@ -6810,7 +7160,7 @@ function handleServeRequest(req, apiKey) {
6810
7160
  function startServeServer(options) {
6811
7161
  const port = options.port ?? DEFAULT_SERVE_PORT;
6812
7162
  const host = options.host ?? SERVE_HOST;
6813
- const apiKey = resolveApiKey(options.apiKey);
7163
+ const apiKey = resolveApiKey();
6814
7164
  const server = Bun.serve({
6815
7165
  hostname: host,
6816
7166
  port,
@@ -6828,6 +7178,7 @@ var init_serve = __esm(() => {
6828
7178
  init_store();
6829
7179
  init_resolve();
6830
7180
  init_config();
7181
+ init_semver();
6831
7182
  if (false) {}
6832
7183
  });
6833
7184
 
@@ -6917,7 +7268,9 @@ var exports_server = {};
6917
7268
  __export(exports_server, {
6918
7269
  startStdioServer: () => startStdioServer,
6919
7270
  startSSEServer: () => startSSEServer,
7271
+ sseAuthToken: () => sseAuthToken,
6920
7272
  createHooksServer: () => createHooksServer,
7273
+ MCP_SSE_HOST: () => MCP_SSE_HOST,
6921
7274
  MCP_PORT: () => MCP_PORT
6922
7275
  });
6923
7276
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -6945,7 +7298,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
6945
7298
  const started = Date.now();
6946
7299
  let ran;
6947
7300
  try {
6948
- ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, env: process.env, timeout: timeoutMs });
7301
+ ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, timeout: timeoutMs });
6949
7302
  } catch (err) {
6950
7303
  try {
6951
7304
  const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
@@ -7051,6 +7404,9 @@ function compactEvent(row) {
7051
7404
  tool_input_preview: row.tool_input ? truncateText(row.tool_input, 180) : undefined
7052
7405
  };
7053
7406
  }
7407
+ function projectLogRows(rows) {
7408
+ return rows.map((row) => projectEventRowForRead({ ...row }));
7409
+ }
7054
7410
  function createHooksServer() {
7055
7411
  const server = new McpServer({
7056
7412
  name: "@hasna/hooks",
@@ -7432,7 +7788,13 @@ function createHooksServer() {
7432
7788
  } catch (err) {
7433
7789
  const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
7434
7790
  if (err instanceof HookTimeoutError2) {
7435
- return { name, decision: "approve", timedOut: true, timeout_ms: effectiveTimeout };
7791
+ return {
7792
+ name,
7793
+ decision: "block",
7794
+ timedOut: true,
7795
+ timeout_ms: effectiveTimeout,
7796
+ error: `hook timed out after ${effectiveTimeout}ms \u2014 preview blocked (never auto-approved)`
7797
+ };
7436
7798
  }
7437
7799
  return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
7438
7800
  }
@@ -7458,15 +7820,25 @@ function createHooksServer() {
7458
7820
  hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
7459
7821
  scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
7460
7822
  }, async ({ agent_type, name, hooks, scope }) => {
7823
+ if (agent_type !== "claude" && agent_type !== "gemini" && agent_type !== "custom") {
7824
+ return {
7825
+ content: [{
7826
+ type: "text",
7827
+ text: JSON.stringify({ error: `unsupported agent_type '${String(agent_type)}' (supported: claude, gemini, custom)` })
7828
+ }],
7829
+ isError: true
7830
+ };
7831
+ }
7461
7832
  const profile = createProfile({ agent_type, name });
7833
+ const target = agent_type === "gemini" ? "gemini" : "claude";
7462
7834
  const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
7463
- const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id }));
7835
+ const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id, target }));
7464
7836
  const installed = results.filter((r) => r.success).map((r) => r.hook);
7465
7837
  const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
7466
7838
  return {
7467
7839
  content: [{
7468
7840
  type: "text",
7469
- text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
7841
+ text: JSON.stringify({ profile, installed, failed, scope, target, run_with: `hooks run <name> --profile ${profile.agent_id}` })
7470
7842
  }]
7471
7843
  };
7472
7844
  });
@@ -7618,12 +7990,13 @@ function createHooksServer() {
7618
7990
  sql += " ORDER BY timestamp DESC LIMIT ?";
7619
7991
  params.push(maxRows);
7620
7992
  const rows = db.query(sql).all(...params);
7993
+ const projected = projectLogRows(rows);
7621
7994
  return {
7622
7995
  content: [{
7623
7996
  type: "text",
7624
7997
  text: JSON.stringify({
7625
- events: compact ? rows.map(compactEvent) : rows,
7626
- count: rows.length,
7998
+ events: compact ? projected.map(compactEvent) : projected,
7999
+ count: projected.length,
7627
8000
  compact,
7628
8001
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7629
8002
  })
@@ -7638,12 +8011,13 @@ function createHooksServer() {
7638
8011
  const db = getDb2();
7639
8012
  const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
7640
8013
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
8014
+ const projected = projectLogRows(rows);
7641
8015
  return {
7642
8016
  content: [{
7643
8017
  type: "text",
7644
8018
  text: JSON.stringify({
7645
- events: compact ? rows.map(compactEvent) : rows,
7646
- count: rows.length,
8019
+ events: compact ? projected.map(compactEvent) : projected,
8020
+ count: projected.length,
7647
8021
  compact,
7648
8022
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7649
8023
  })
@@ -7668,12 +8042,13 @@ function createHooksServer() {
7668
8042
  }
7669
8043
  const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
7670
8044
  const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
8045
+ const projected = projectLogRows(rows);
7671
8046
  return {
7672
8047
  content: [{
7673
8048
  type: "text",
7674
8049
  text: JSON.stringify({
7675
- events: compact ? rows.map(compactEvent) : rows,
7676
- count: rows.length,
8050
+ events: compact ? projected.map(compactEvent) : projected,
8051
+ count: projected.length,
7677
8052
  compact,
7678
8053
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7679
8054
  })
@@ -7801,11 +8176,48 @@ function createHooksServer() {
7801
8176
  });
7802
8177
  return server;
7803
8178
  }
7804
- async function startSSEServer(port = MCP_PORT) {
8179
+ function isLoopbackHost(host) {
8180
+ return host === "" || host === "127.0.0.1" || host === "::1" || host === "localhost";
8181
+ }
8182
+ function sseAuthToken(explicit) {
8183
+ if (explicit && explicit.length > 0)
8184
+ return explicit;
8185
+ for (const name of MCP_SSE_AUTH_ENV) {
8186
+ const value = process.env[name];
8187
+ if (value && value.length > 0)
8188
+ return value;
8189
+ }
8190
+ return;
8191
+ }
8192
+ async function startSSEServer(options = {}) {
8193
+ const port = options.port ?? MCP_PORT;
8194
+ const host = options.host ?? MCP_SSE_HOST;
8195
+ const authToken = sseAuthToken(options.authToken);
8196
+ const loopback = isLoopbackHost(host);
8197
+ if (!loopback && !authToken) {
8198
+ 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.`);
8199
+ }
7805
8200
  const server = createHooksServer();
7806
8201
  const transports = new Map;
8202
+ function authorized2(headers) {
8203
+ if (!authToken)
8204
+ return false;
8205
+ const rawAuth = headers["authorization"];
8206
+ const header = (Array.isArray(rawAuth) ? rawAuth[0] : rawAuth) ?? "";
8207
+ if (header.startsWith("Bearer "))
8208
+ return secureEqual(header.slice("Bearer ".length), authToken);
8209
+ const rawKey = headers["x-api-key"];
8210
+ return secureEqual((Array.isArray(rawKey) ? rawKey[0] : rawKey) ?? "", authToken);
8211
+ }
7807
8212
  const httpServer = createServer(async (req, res) => {
7808
8213
  const url = new URL(req.url || "/", `http://localhost:${port}`);
8214
+ if (url.pathname === "/sse" || url.pathname === "/messages") {
8215
+ if (authToken && !authorized2(req.headers)) {
8216
+ res.writeHead(401);
8217
+ res.end("unauthorized: valid MCP token required");
8218
+ return;
8219
+ }
8220
+ }
7809
8221
  if (url.pathname === "/sse") {
7810
8222
  const transport = new SSEServerTransport("/messages", res);
7811
8223
  transports.set(transport.sessionId, transport);
@@ -7828,9 +8240,9 @@ async function startSSEServer(port = MCP_PORT) {
7828
8240
  res.end(JSON.stringify({ name: "@hasna/hooks", version: pkg.version, transport: "sse", port }));
7829
8241
  }
7830
8242
  });
7831
- httpServer.listen(port, () => {
7832
- console.error(`@hasna/hooks MCP server running on http://localhost:${port}`);
7833
- console.error(`SSE endpoint: http://localhost:${port}/sse`);
8243
+ httpServer.listen(port, host, () => {
8244
+ console.error(`@hasna/hooks MCP server running on http://${host}:${port}`);
8245
+ console.error(`SSE endpoint: http://${host}:${port}/sse`);
7834
8246
  });
7835
8247
  }
7836
8248
  async function startStdioServer() {
@@ -7844,7 +8256,7 @@ async function startStdioServer() {
7844
8256
  process.exit(1);
7845
8257
  }
7846
8258
  }
7847
- var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents;
8259
+ var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents, MCP_SSE_HOST = "127.0.0.1", MCP_SSE_AUTH_ENV;
7848
8260
  var init_server = __esm(() => {
7849
8261
  init_registry();
7850
8262
  init_installer();
@@ -7852,6 +8264,7 @@ var init_server = __esm(() => {
7852
8264
  init_manifest();
7853
8265
  init_resolve();
7854
8266
  init_store();
8267
+ init_redact();
7855
8268
  init_storage();
7856
8269
  __dirname3 = dirname5(fileURLToPath3(import.meta.url));
7857
8270
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
@@ -7865,6 +8278,7 @@ var init_server = __esm(() => {
7865
8278
  }
7866
8279
  } catch {}
7867
8280
  _hooksAgents = new Map;
8281
+ MCP_SSE_AUTH_ENV = ["HASNA_HOOKS_MCP_TOKEN", "HOOKS_MCP_TOKEN"];
7868
8282
  });
7869
8283
 
7870
8284
  // src/mcp/http.ts
@@ -7926,7 +8340,7 @@ function startMcpHttpServer(options) {
7926
8340
  var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
7927
8341
  var init_http = () => {};
7928
8342
 
7929
- // ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
8343
+ // node_modules/@hasna/events/dist/commander.js
7930
8344
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
7931
8345
  import { Buffer as Buffer2 } from "buffer";
7932
8346
  import { existsSync } from "fs";
@@ -9111,7 +9525,7 @@ function replaySummary(events, deliveries, nextCursor) {
9111
9525
  // src/cli/index.tsx
9112
9526
  import { render } from "ink";
9113
9527
 
9114
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
9528
+ // node_modules/commander/esm.mjs
9115
9529
  var import__ = __toESM(require_commander(), 1);
9116
9530
  var {
9117
9531
  program,
@@ -9137,11 +9551,11 @@ import { fileURLToPath as fileURLToPath4 } from "url";
9137
9551
  import { useState as useState7 } from "react";
9138
9552
  import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
9139
9553
 
9140
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
9554
+ // node_modules/ink-select-input/build/Indicator.js
9141
9555
  import React from "react";
9142
9556
  import { Box, Text } from "ink";
9143
9557
 
9144
- // ../../node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
9558
+ // node_modules/is-unicode-supported/index.js
9145
9559
  import process2 from "process";
9146
9560
  function isUnicodeSupported() {
9147
9561
  const { env } = process2;
@@ -9152,7 +9566,7 @@ function isUnicodeSupported() {
9152
9566
  return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
9153
9567
  }
9154
9568
 
9155
- // ../../node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
9569
+ // node_modules/figures/index.js
9156
9570
  var common = {
9157
9571
  circleQuestionMark: "(?)",
9158
9572
  questionMarkPrefix: "(?)",
@@ -9428,23 +9842,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
9428
9842
  var figures_default = figures;
9429
9843
  var replacements = Object.entries(specialMainSymbols);
9430
9844
 
9431
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
9845
+ // node_modules/ink-select-input/build/Indicator.js
9432
9846
  function Indicator({ isSelected = false }) {
9433
9847
  return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
9434
9848
  }
9435
9849
  var Indicator_default = Indicator;
9436
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
9850
+ // node_modules/ink-select-input/build/Item.js
9437
9851
  import * as React2 from "react";
9438
9852
  import { Text as Text2 } from "ink";
9439
9853
  function Item({ isSelected = false, label }) {
9440
9854
  return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
9441
9855
  }
9442
9856
  var Item_default = Item;
9443
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
9857
+ // node_modules/ink-select-input/build/SelectInput.js
9444
9858
  import { isDeepStrictEqual } from "util";
9445
9859
  import React3, { useState, useEffect, useRef, useCallback } from "react";
9446
9860
 
9447
- // ../../node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
9861
+ // node_modules/to-rotated/index.js
9448
9862
  function toRotated(array, steps) {
9449
9863
  if (!Array.isArray(array)) {
9450
9864
  throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
@@ -9466,7 +9880,7 @@ function toRotated(array, steps) {
9466
9880
  ];
9467
9881
  }
9468
9882
 
9469
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
9883
+ // node_modules/ink-select-input/build/SelectInput.js
9470
9884
  import { Box as Box2, useInput } from "ink";
9471
9885
  function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
9472
9886
  const hasLimit = typeof customLimit === "number" && items.length > customLimit;
@@ -9762,7 +10176,7 @@ function HookSelect({
9762
10176
  import { useState as useState4, useEffect as useEffect3 } from "react";
9763
10177
  import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
9764
10178
 
9765
- // ../../node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
10179
+ // node_modules/ink-text-input/build/index.js
9766
10180
  import React5, { useState as useState3, useEffect as useEffect2 } from "react";
9767
10181
  import { Text as Text6, useInput as useInput3 } from "ink";
9768
10182
  import chalk from "chalk";
@@ -9940,7 +10354,7 @@ function SearchView({
9940
10354
  import { useState as useState6, useEffect as useEffect5 } from "react";
9941
10355
  import { Box as Box7, Text as Text9 } from "ink";
9942
10356
 
9943
- // ../../node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
10357
+ // node_modules/ink-spinner/build/index.js
9944
10358
  var import_cli_spinners = __toESM(require_cli_spinners(), 1);
9945
10359
  import React7, { useState as useState5, useEffect as useEffect4 } from "react";
9946
10360
  import { Text as Text8 } from "ink";
@@ -10305,10 +10719,12 @@ function App({ initialHooks, overwrite = false }) {
10305
10719
  // src/cli/index.tsx
10306
10720
  init_registry();
10307
10721
  init_installer();
10722
+ init_redact();
10308
10723
  init_profiles();
10309
10724
  init_manifest();
10310
10725
  init_resolve();
10311
10726
  init_store();
10727
+ init_semver();
10312
10728
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
10313
10729
  var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
10314
10730
  var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
@@ -10340,6 +10756,9 @@ function truncateText2(value, max = 96) {
10340
10756
  return text;
10341
10757
  return `${text.slice(0, Math.max(0, max - 3))}...`;
10342
10758
  }
10759
+ function projectLogRows2(rows) {
10760
+ return rows.map((row) => projectEventRowForRead({ ...row }));
10761
+ }
10343
10762
  function readToken(tokenFile) {
10344
10763
  if (tokenFile) {
10345
10764
  try {
@@ -10601,6 +11020,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
10601
11020
  console.log(chalk2.red(`Unknown category: ${options.category}`));
10602
11021
  console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
10603
11022
  }
11023
+ process.exitCode = 1;
10604
11024
  return;
10605
11025
  }
10606
11026
  toInstall = getHooksByCategory(category).map((h) => h.name);
@@ -10633,7 +11053,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
10633
11053
  const version = arg.slice(at + 1);
10634
11054
  if (!HOOK_NAME_RE2.test(name))
10635
11055
  return null;
10636
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
11056
+ if (!SEMVER_PATTERN.test(version))
10637
11057
  return null;
10638
11058
  return { name, version };
10639
11059
  }
@@ -10767,7 +11187,7 @@ Dry run \u2014 would install (${scope}, ${target}):
10767
11187
  target,
10768
11188
  applied: results.some((r) => r.applied)
10769
11189
  }));
10770
- if (results.length > 0 && results.every((r) => !r.success)) {
11190
+ if (results.some((r) => !r.success)) {
10771
11191
  process.exitCode = 1;
10772
11192
  }
10773
11193
  return;
@@ -10805,6 +11225,11 @@ Installing hooks (${scope}, ${target})...
10805
11225
  process.exitCode = 1;
10806
11226
  return;
10807
11227
  }
11228
+ if (successCount < results.length) {
11229
+ console.log(chalk2.red(`
11230
+ \u2717 ${results.length - successCount} of ${results.length} hook(s) failed to install.`));
11231
+ process.exitCode = 1;
11232
+ }
10808
11233
  console.log(chalk2.dim(`
10809
11234
  Registered in ${settingsFile}`));
10810
11235
  });
@@ -11000,6 +11425,7 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
11000
11425
  } else {
11001
11426
  console.log(chalk2.red(`Hook '${hook}' not found${hint}`));
11002
11427
  }
11428
+ process.exitCode = 1;
11003
11429
  return;
11004
11430
  }
11005
11431
  const globalInstalled = getRegisteredHooks("global").includes(meta.name);
@@ -11046,6 +11472,13 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11046
11472
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
11047
11473
  }
11048
11474
  const registered = getRegisteredHooks(scope);
11475
+ let wiringCount = 0;
11476
+ if (settingsExist) {
11477
+ try {
11478
+ const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
11479
+ wiringCount = countSettingsWiring(settings);
11480
+ } catch {}
11481
+ }
11049
11482
  for (const name of registered) {
11050
11483
  const custom = readCustomManifest(name);
11051
11484
  const meta = custom ? resolveHookMeta(name) : getHook(name);
@@ -11083,7 +11516,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11083
11516
  }
11084
11517
  }
11085
11518
  if (options.json) {
11086
- console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
11519
+ console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, wiring_count: wiringCount, scope }));
11087
11520
  return;
11088
11521
  }
11089
11522
  console.log(chalk2.bold(`
@@ -11092,6 +11525,9 @@ Hook Health Check (${scope})
11092
11525
  if (registered.length === 0) {
11093
11526
  console.log(chalk2.dim(" No hooks registered."));
11094
11527
  console.log(chalk2.dim(" Run: hooks install gitguard"));
11528
+ 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)`));
11529
+ if (issues.length > 0)
11530
+ process.exitCode = 1;
11095
11531
  return;
11096
11532
  }
11097
11533
  if (healthy.length > 0) {
@@ -11100,6 +11536,9 @@ Hook Health Check (${scope})
11100
11536
  console.log(chalk2.green(` ${name}`));
11101
11537
  }
11102
11538
  }
11539
+ if (registered.length > 0) {
11540
+ 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)`));
11541
+ }
11103
11542
  if (issues.length > 0) {
11104
11543
  console.log();
11105
11544
  for (const issue of issues) {
@@ -11107,9 +11546,11 @@ Hook Health Check (${scope})
11107
11546
  console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
11108
11547
  }
11109
11548
  }
11110
- if (issues.length === 0) {
11549
+ if (issues.length === 0 && registered.length > 0) {
11111
11550
  console.log(chalk2.green(`
11112
- All hooks healthy!`));
11551
+ All ${healthy.length} registered hook(s) healthy`));
11552
+ } else if (issues.some((i) => i.severity === "error")) {
11553
+ process.exitCode = 1;
11113
11554
  }
11114
11555
  console.log();
11115
11556
  });
@@ -11142,7 +11583,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
11142
11583
  const version = arg.slice(at + 1);
11143
11584
  if (!HOOK_NAME_RE2.test(name))
11144
11585
  return null;
11145
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
11586
+ if (!SEMVER_PATTERN.test(version))
11146
11587
  return null;
11147
11588
  return { name, version };
11148
11589
  }
@@ -11225,6 +11666,7 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
11225
11666
  } else {
11226
11667
  console.log(chalk2.red(`Hook '${hook}' not found`));
11227
11668
  }
11669
+ process.exitCode = 1;
11228
11670
  return;
11229
11671
  }
11230
11672
  const hookPath = getHookPath(hook);
@@ -11479,18 +11921,19 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
11479
11921
  sql += " ORDER BY timestamp DESC LIMIT ?";
11480
11922
  params.push(String(limit));
11481
11923
  const rows = db.query(sql).all(...params);
11924
+ const projected = projectLogRows2(rows);
11482
11925
  if (options.json) {
11483
- console.log(JSON.stringify(rows, null, 2));
11926
+ console.log(JSON.stringify(projected, null, 2));
11484
11927
  return;
11485
11928
  }
11486
- if (rows.length === 0) {
11929
+ if (projected.length === 0) {
11487
11930
  console.log(chalk2.dim("No events found."));
11488
11931
  return;
11489
11932
  }
11490
11933
  console.log(chalk2.bold(`
11491
- Hook Events (${rows.length})
11934
+ Hook Events (${projected.length})
11492
11935
  `));
11493
- for (const row of rows) {
11936
+ for (const row of projected) {
11494
11937
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11495
11938
  const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
11496
11939
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
@@ -11505,18 +11948,19 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
11505
11948
  const limit = parseInt(options.limit) || 50;
11506
11949
  const q = `%${text}%`;
11507
11950
  const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
11951
+ const projected = projectLogRows2(rows);
11508
11952
  if (options.json) {
11509
- console.log(JSON.stringify(rows, null, 2));
11953
+ console.log(JSON.stringify(projected, null, 2));
11510
11954
  return;
11511
11955
  }
11512
- if (rows.length === 0) {
11956
+ if (projected.length === 0) {
11513
11957
  console.log(chalk2.dim(`No events matching "${text}".`));
11514
11958
  return;
11515
11959
  }
11516
11960
  console.log(chalk2.bold(`
11517
- Search results for "${text}" (${rows.length})
11961
+ Search results for "${text}" (${projected.length})
11518
11962
  `));
11519
- for (const row of rows) {
11963
+ for (const row of projected) {
11520
11964
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11521
11965
  const snippet = truncateText2(row.tool_input || row.error || "", 80);
11522
11966
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
@@ -11529,18 +11973,19 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
11529
11973
  const db = getDb2();
11530
11974
  const limit = parseInt(options.n) || 20;
11531
11975
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
11976
+ const projected = projectLogRows2(rows);
11532
11977
  if (options.json) {
11533
- console.log(JSON.stringify(rows, null, 2));
11978
+ console.log(JSON.stringify(projected, null, 2));
11534
11979
  return;
11535
11980
  }
11536
- if (rows.length === 0) {
11981
+ if (projected.length === 0) {
11537
11982
  console.log(chalk2.dim("No events yet."));
11538
11983
  return;
11539
11984
  }
11540
11985
  console.log(chalk2.bold(`
11541
- Last ${rows.length} events
11986
+ Last ${projected.length} events
11542
11987
  `));
11543
- for (const row of rows) {
11988
+ for (const row of projected) {
11544
11989
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11545
11990
  const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
11546
11991
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
@@ -11573,18 +12018,19 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
11573
12018
  }
11574
12019
  const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
11575
12020
  const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
12021
+ const projected = projectLogRows2(rows);
11576
12022
  if (options.json) {
11577
- console.log(JSON.stringify(rows, null, 2));
12023
+ console.log(JSON.stringify(projected, null, 2));
11578
12024
  return;
11579
12025
  }
11580
- if (rows.length === 0) {
12026
+ if (projected.length === 0) {
11581
12027
  console.log(chalk2.dim(`No errors in the last ${options.since}.`));
11582
12028
  return;
11583
12029
  }
11584
12030
  console.log(chalk2.bold(`
11585
- Errors (last ${options.since}, ${rows.length} found)
12031
+ Errors (last ${options.since}, ${projected.length} found)
11586
12032
  `));
11587
- for (const row of rows) {
12033
+ for (const row of projected) {
11588
12034
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11589
12035
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
11590
12036
  }
@@ -11706,18 +12152,17 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
11706
12152
  console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
11707
12153
  console.log(chalk2.dim(` sha256 ${result.actual}`));
11708
12154
  });
11709
- program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").option("--api-key <key>", "API key for publish (defaults to HASNA_HOOKS_API_KEY / HOOKS_API_KEY)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock)").action(async (options) => {
12155
+ 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) => {
11710
12156
  const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
11711
12157
  startServeServer2({
11712
12158
  port: options.port ? parseInt(options.port, 10) : undefined,
11713
- host: options.host,
11714
- apiKey: options.apiKey
12159
+ host: options.host
11715
12160
  });
11716
12161
  });
11717
12162
  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) => {
11718
12163
  const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
11719
12164
  try {
11720
- const plan = options.dryRun ? await planSync2() : await syncHooks2();
12165
+ const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
11721
12166
  const diff = plan.diff;
11722
12167
  if (options.json) {
11723
12168
  console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
@@ -11813,7 +12258,7 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
11813
12258
  await startStdioServer2();
11814
12259
  } else if (options.sse) {
11815
12260
  const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
11816
- await startSSEServer2(options.port ? parseInt(options.port) : 39427);
12261
+ await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
11817
12262
  } else {
11818
12263
  const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
11819
12264
  const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));