@hasna/hooks 0.10.7 → 0.10.9

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
@@ -1079,6 +1079,19 @@ function pinEligibleFile(file, maximum, executable = false) {
1079
1079
  closeSync(fd);
1080
1080
  }
1081
1081
  }
1082
+ function pinVerified(file, expected, maximum, executable = false) {
1083
+ try {
1084
+ return pinEligibleFile(file, maximum, executable) === expected;
1085
+ } catch {
1086
+ return false;
1087
+ }
1088
+ }
1089
+ function nativeSafetyBindingVerified(command) {
1090
+ const binding = nativeSafetyCommandBinding(command);
1091
+ if (!binding)
1092
+ return;
1093
+ return pinVerified(binding.runtime, binding.runtimeSHA256, 200000000, true) && pinVerified(binding.worker, binding.workerSHA256, 256000);
1094
+ }
1082
1095
  function installedNativeSafetyCommand(name) {
1083
1096
  const home = realpathSync2(homedir2());
1084
1097
  const runtime = realpathSync2(process.execPath);
@@ -2068,7 +2081,7 @@ var init_codex_settings = __esm(() => {
2068
2081
  snapshots = new WeakMap;
2069
2082
  });
2070
2083
 
2071
- // ../../node_modules/.bun/@hasna+skills@0.9.14+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
2084
+ // ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
2072
2085
  import { existsSync as existsSync9 } from "fs";
2073
2086
  import { homedir as homedir4 } from "os";
2074
2087
  import { join as join8, resolve as resolve3 } from "path";
@@ -2081,6 +2094,7 @@ import { basename as basename2, isAbsolute as isAbsolute22, resolve as resolve22
2081
2094
  import { createHash as createHash16 } from "crypto";
2082
2095
  import { lstatSync as lstatSync20, readFileSync as readFileSync16, readdirSync as readdirSync9, realpathSync as realpathSync4 } from "fs";
2083
2096
  import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute14, join as join26, resolve as resolve14, sep as sep5 } from "path";
2097
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
2084
2098
  import { dirname as dirname32, isAbsolute as isAbsolute3, join as join72, resolve as resolve32 } from "path";
2085
2099
  import { lstatSync as lstatSync3, readFileSync as readFileSync6, opendirSync } from "fs";
2086
2100
  import { createHash as createHash22 } from "crypto";
@@ -2579,8 +2593,8 @@ function assertAgentPolicyCollections(policy) {
2579
2593
  text(source.path);
2580
2594
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
2581
2595
  if (source.hashMode !== undefined)
2582
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
2583
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
2596
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
2597
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
2584
2598
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "settings.json");
2585
2599
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
2586
2600
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -3494,7 +3508,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
3494
3508
  }
3495
3509
  need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
3496
3510
  }
3497
- function inferencePreference(key, child) {
3511
+ function inferencePreference(key, child, version) {
3498
3512
  if (key === "skipDangerousModePermissionPrompt") {
3499
3513
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
3500
3514
  return false;
@@ -3507,7 +3521,7 @@ function inferencePreference(key, child) {
3507
3521
  return false;
3508
3522
  need2(child.kind === "object", "requires an object of model preferences");
3509
3523
  child.entries = child.entries.filter(([model, preferences]) => {
3510
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
3524
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
3511
3525
  return true;
3512
3526
  const effort = preferences.entries[0][1];
3513
3527
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -3518,7 +3532,7 @@ function inferencePreference(key, child) {
3518
3532
  function settingsDigest(text2, version = 1) {
3519
3533
  const value = parse3(text2);
3520
3534
  value.entries = value.entries.filter(([key, child]) => {
3521
- if (version === 2 && inferencePreference(key, child))
3535
+ if (version !== 1 && inferencePreference(key, child, version))
3522
3536
  return false;
3523
3537
  if (key === "model") {
3524
3538
  need2(child.kind === "string", "contains an invalid model selection");
@@ -3556,6 +3570,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
3556
3570
  budget.remaining -= bytes;
3557
3571
  return settingsDigest(text2, 2);
3558
3572
  }
3573
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
3574
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
3575
+ }
3576
+ function hashClaudeSettingsReplacementV3(text2, budget) {
3577
+ need2(typeof text2 === "string", "requires settings text");
3578
+ const bytes = Buffer.byteLength(text2);
3579
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
3580
+ budget.remaining -= bytes;
3581
+ return settingsDigest(text2, 3);
3582
+ }
3559
3583
  function assertSafePath(path) {
3560
3584
  let cursor = resolve9(path);
3561
3585
  while (true) {
@@ -4007,6 +4031,8 @@ function applyAgentIntegration(plan) {
4007
4031
  parseManagedSkillPolicy(change.after);
4008
4032
  const aliases = plan.rootAliases ?? [];
4009
4033
  recheckRootAliases(aliases);
4034
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
4035
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
4010
4036
  for (const agent of plan.managedAgentChecks?.agents ?? [])
4011
4037
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
4012
4038
  for (const binding of plan.discoveryBefore ?? []) {
@@ -5009,10 +5035,11 @@ function unchanged2(before) {
5009
5035
  need4(sameStat(before.stat, after.stat) && after.sha256 === before.sha256, "INPUT_CHANGED");
5010
5036
  }
5011
5037
  function assertCodexHookDiscoveryRecovery(binding, witness) {
5012
- const proof = proofs.get(witness);
5038
+ const proof = proofs2.get(witness);
5013
5039
  need4(!!proof && binding.agent === "codex" && witness.discoverySha256 === sha2(JSON.stringify(binding)), "RECONCILE_DISCOVERY_RECOVERY_CHANGED");
5014
5040
  unchanged2(proof.before);
5015
5041
  unchanged2(proof.current);
5042
+ proof.recheckAdditions();
5016
5043
  }
5017
5044
  function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witness) {
5018
5045
  if (!witness || source.path !== witness.configPath || source.hashMode !== "bytes")
@@ -5053,13 +5080,13 @@ function read2(path, changes) {
5053
5080
  return readFileSync16(path, "utf8");
5054
5081
  }
5055
5082
  function projected(source, changes, budget = discoveryByteBudget()) {
5056
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
5083
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
5057
5084
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
5058
5085
  throw new Error("Claude settings witnesses require exact typed metadata");
5059
- const current2 = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
5086
+ const current2 = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
5060
5087
  if (current2 !== source.sha256)
5061
5088
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
5062
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
5089
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
5063
5090
  }
5064
5091
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
5065
5092
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -5121,7 +5148,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
5121
5148
  safe4(root2);
5122
5149
  }
5123
5150
  function assertMarketplaceBinding(binding, source) {
5124
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
5151
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
5125
5152
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
5126
5153
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
5127
5154
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -5130,7 +5157,10 @@ function rebindAgentDiscovery(binding, changes) {
5130
5157
  const budget = discoveryByteBudget();
5131
5158
  return { ...binding, sources: binding.sources.map((source) => {
5132
5159
  assertMarketplaceBinding(binding, source);
5133
- return { ...source, sha256: projected(source, changes, budget) };
5160
+ const sha2562 = projected(source, changes, budget);
5161
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
5162
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
5163
+ return { ...source, sha256: sha2562 };
5134
5164
  }) };
5135
5165
  }
5136
5166
  function assertProjectDiscovery(agent, directories, home, canonical4 = resolve14, reviewed) {
@@ -5242,6 +5272,19 @@ function resolveAgentDiscovery(options) {
5242
5272
  }
5243
5273
  }
5244
5274
  }
5275
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
5276
+ const retained = options.retainedReview;
5277
+ if (retained.agent !== agent || retained.method !== "reviewed")
5278
+ unresolved("invalid retained review");
5279
+ verifyAgentDiscovery(retained);
5280
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
5281
+ unresolved("retained review is missing the full agent configuration source");
5282
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
5283
+ unresolved("retained settings witness names another configuration source");
5284
+ if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
5285
+ unresolved("retained review is missing current runtime discovery coverage");
5286
+ return retained;
5287
+ }
5245
5288
  if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
5246
5289
  throw new Error("Invalid --discovery-inputs version or agents");
5247
5290
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -5252,7 +5295,7 @@ function resolveAgentDiscovery(options) {
5252
5295
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
5253
5296
  throw new Error("Explicit discovery reviews require full source-file hashes");
5254
5297
  verifyAgentDiscovery(supplied);
5255
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
5298
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
5256
5299
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
5257
5300
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
5258
5301
  throw new Error("Discovery review must include the agent configuration source");
@@ -12042,7 +12085,7 @@ Use the Skills CLI as the skill authority for this station.
12042
12085
  - Run a skill only when the user's task calls for execution. Use \`skills run --help\` and select the intended local or cloud target explicitly.
12043
12086
 
12044
12087
  This bridge contains no skill payloads. Do not copy skill content into agent-native skill directories or load other native skill copies. If a Skills command refuses authority, selection, integrity, or native drift, report that refusal and use its repair guidance; do not substitute bundled or stale local content. Explicit local draft authoring remains available.
12045
- `, CLI_BRIDGE_DIGEST, CLI_BRIDGE_FILES, CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, CLAUDE_COORDINATED_HOOK_EVENTS, PERSISTED_EFFORT_LEVELS, sha = (data) => createHash10("sha256").update(data).digest("hex"), ROOTS, PLUGIN_PROJECTION_LIMITS, pluginHash = (bytes) => createHash11("sha256").update(bytes).digest("hex"), READ_FLAGS, WRITE_FLAGS, uid = () => typeof process.getuid === "function" ? BigInt(process.getuid()) : undefined, missing = (error) => Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"), quote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`, resolverGuard = 'exec 9<"$1"; actual=$(/usr/bin/sha256sum /proc/self/fd/9); actual=${actual%% *}; [ "sha256:$actual" = "$2" ]; exec /proc/self/fd/9 integration plugin resolve --binding "$3"', CLAUDE_MARKETPLACE_REGISTRY_LIMITS, sameFile2 = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, field = (object4, key) => object4.entries.find(([name]) => name === key)?.[1], sameStat = (a, b) => ["dev", "ino", "uid", "nlink", "mode", "size", "mtimeNs", "ctimeNs"].every((key) => a[key] === b[key]), sha2 = (text2) => createHash15("sha256").update(text2).digest("hex"), proofs, digest = (text2) => createHash16("sha256").update(text2).digest("hex"), NATIVE_DISCOVERY_ROOTS, OFFICIAL_SKILL_NAMES, exports_external, exports_core2, exports_util, EVALUATING, captureStackTrace, allowsEval, getParsedType = (data) => {
12088
+ `, CLI_BRIDGE_DIGEST, CLI_BRIDGE_FILES, CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, CLAUDE_COORDINATED_HOOK_EVENTS, PERSISTED_EFFORT_LEVELS, sha = (data) => createHash10("sha256").update(data).digest("hex"), ROOTS, PLUGIN_PROJECTION_LIMITS, pluginHash = (bytes) => createHash11("sha256").update(bytes).digest("hex"), READ_FLAGS, WRITE_FLAGS, uid = () => typeof process.getuid === "function" ? BigInt(process.getuid()) : undefined, missing = (error) => Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"), quote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`, resolverGuard = 'exec 9<"$1"; actual=$(/usr/bin/sha256sum /proc/self/fd/9); actual=${actual%% *}; [ "sha256:$actual" = "$2" ]; exec /proc/self/fd/9 integration plugin resolve --binding "$3"', CLAUDE_MARKETPLACE_REGISTRY_LIMITS, sameFile2 = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, field = (object4, key) => object4.entries.find(([name]) => name === key)?.[1], sameStat = (a, b) => ["dev", "ino", "uid", "nlink", "mode", "size", "mtimeNs", "ctimeNs"].every((key) => a[key] === b[key]), events, matchedEvents, proofs, sha2 = (text2) => createHash15("sha256").update(text2).digest("hex"), proofs2, digest = (text2) => createHash16("sha256").update(text2).digest("hex"), NATIVE_DISCOVERY_ROOTS, OFFICIAL_SKILL_NAMES, exports_external, exports_core2, exports_util, EVALUATING, captureStackTrace, allowsEval, getParsedType = (data) => {
12046
12089
  const t2 = typeof data;
12047
12090
  switch (t2) {
12048
12091
  case "undefined":
@@ -26445,7 +26488,7 @@ ${end.comment}` : end.comment;
26445
26488
  $visitAsync = visit.visitAsync;
26446
26489
  package_default = {
26447
26490
  name: "@hasna/skills",
26448
- version: "0.9.14",
26491
+ version: "0.9.17",
26449
26492
  description: "Skills library for AI coding agents",
26450
26493
  type: "module",
26451
26494
  bin: {
@@ -26817,7 +26860,10 @@ ${end.comment}` : end.comment;
26817
26860
  READ_FLAGS = constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
26818
26861
  WRITE_FLAGS = constants8.O_WRONLY | constants8.O_CREAT | constants8.O_EXCL | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
26819
26862
  CLAUDE_MARKETPLACE_REGISTRY_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
26863
+ events = { UserPromptSubmit: ["userPromptSubmit", "user_prompt_submit"], SessionStart: ["sessionStart", "session_start"], SubagentStart: ["subagentStart", "subagent_start"] };
26864
+ matchedEvents = { ...events, PreToolUse: ["preToolUse", "pre_tool_use"] };
26820
26865
  proofs = new WeakMap;
26866
+ proofs2 = new WeakMap;
26821
26867
  NATIVE_DISCOVERY_ROOTS = NATIVE_SKILL_ROOTS.map(([, path]) => path.split("/"));
26822
26868
  OFFICIAL_SKILL_NAMES = new Set(SKILLS.map((skill) => skill.name));
26823
26869
  exports_external = {};
@@ -32999,8 +33045,8 @@ CREATE TABLE IF NOT EXISTS skills_sync_cursors (
32999
33045
  import { createHash as createHash5 } from "crypto";
33000
33046
  function coordinateClaudeSettings(before, replacement, options) {
33001
33047
  const prior = JSON.parse(before ?? "{}"), next = JSON.parse(replacement);
33002
- const events = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
33003
- const changed = [...events].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
33048
+ const events2 = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
33049
+ const changed = [...events2].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
33004
33050
  const selected = changed.length ? changed : ["PreToolUse"];
33005
33051
  const plan = planClaudeHookEventsUpdate({
33006
33052
  ...options,
@@ -33270,7 +33316,7 @@ function detectConflict(name, scope, target) {
33270
33316
  const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
33271
33317
  if (!meta3 || !meta3.matcher)
33272
33318
  return;
33273
- const events = new Set(getHookEvents(meta3));
33319
+ const events2 = new Set(getHookEvents(meta3));
33274
33320
  const registered = getRegisteredHooksForTarget(scope, target);
33275
33321
  for (const existingName of registered) {
33276
33322
  if (existingName === name)
@@ -33278,7 +33324,7 @@ function detectConflict(name, scope, target) {
33278
33324
  const existing = getHook(existingName);
33279
33325
  if (!existing || !existing.matcher)
33280
33326
  continue;
33281
- if (!getHookEvents(existing).some((event) => events.has(event)))
33327
+ if (!getHookEvents(existing).some((event) => events2.has(event)))
33282
33328
  continue;
33283
33329
  if (matchersOverlap(meta3.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
33284
33330
  return `conflicts with '${existingName}' (same event ${meta3.event}, overlapping matcher '${existing.matcher}')`;
@@ -37436,10 +37482,10 @@ var init_redact = __esm(() => {
37436
37482
 
37437
37483
  // src/lib/profiles.ts
37438
37484
  import { existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync12, readdirSync as readdirSync8, rmSync, cpSync as cpSync2 } from "fs";
37439
- import { join as join27 } from "path";
37485
+ import { join as join29 } from "path";
37440
37486
  function getProfilesDir() {
37441
- const newDir = join27(getEffectiveDataRoot(), "profiles");
37442
- const oldDir = join27(getHomeDir(), ".hooks", "profiles");
37487
+ const newDir = join29(getEffectiveDataRoot(), "profiles");
37488
+ const oldDir = join29(getHomeDir(), ".hooks", "profiles");
37443
37489
  if (!existsSync13(newDir) && existsSync13(oldDir)) {
37444
37490
  mkdirSync13(newDir, { recursive: true });
37445
37491
  cpSync2(oldDir, newDir, { recursive: true });
@@ -37453,7 +37499,7 @@ function ensureProfilesDir() {
37453
37499
  }
37454
37500
  }
37455
37501
  function profilePath(id) {
37456
- return join27(getProfilesDir(), `${id}.json`);
37502
+ return join29(getProfilesDir(), `${id}.json`);
37457
37503
  }
37458
37504
  function shortUuid() {
37459
37505
  return crypto.randomUUID().slice(0, 8);
@@ -37495,7 +37541,7 @@ function listProfiles() {
37495
37541
  const profiles = [];
37496
37542
  for (const file2 of files) {
37497
37543
  try {
37498
- const content = readFileSync20(join27(dir, file2), "utf-8");
37544
+ const content = readFileSync20(join29(dir, file2), "utf-8");
37499
37545
  profiles.push(JSON.parse(content));
37500
37546
  } catch {}
37501
37547
  }
@@ -37550,7 +37596,7 @@ import { createRequire as createRequire3 } from "module";
37550
37596
  import { fileURLToPath as fileURLToPath22 } from "url";
37551
37597
  import { closeSync as closeSync18, fstatSync as fstatSync17, lstatSync as lstatSync24, openSync as openSync18, readFileSync as readFileSync21, realpathSync as realpathSync13, statSync as statSync2 } from "fs";
37552
37598
  import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
37553
- import { basename as basename9, dirname as dirname23, join as join29, relative as relative3, sep as sep3 } from "path";
37599
+ import { basename as basename9, dirname as dirname23, join as join30, relative as relative3, sep as sep6 } from "path";
37554
37600
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
37555
37601
  function envToken2(name) {
37556
37602
  return name.toUpperCase().replace(/-/g, "_");
@@ -37625,7 +37671,7 @@ function o(e2, r2, t2) {
37625
37671
  }
37626
37672
  function entryPoint(directory2) {
37627
37673
  const root = realpathSync13(directory2);
37628
- const fd = openSync18(join29(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
37674
+ const fd = openSync18(join30(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
37629
37675
  let pkg;
37630
37676
  try {
37631
37677
  const stat2 = fstatSync17(fd);
@@ -37645,9 +37691,9 @@ function entryPoint(directory2) {
37645
37691
  if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
37646
37692
  throw new Error("Invalid Secrets import export");
37647
37693
  }
37648
- const file2 = realpathSync13(join29(root, ...parts));
37694
+ const file2 = realpathSync13(join30(root, ...parts));
37649
37695
  const within = relative3(root, file2);
37650
- if (!within || within === ".." || within.startsWith(`..${sep3}`) || !statSync2(file2).isFile()) {
37696
+ if (!within || within === ".." || within.startsWith(`..${sep6}`) || !statSync2(file2).isFile()) {
37651
37697
  throw new Error("Secrets import export escapes its package or is not a file");
37652
37698
  }
37653
37699
  return pathToFileURL(file2).href;
@@ -37656,7 +37702,7 @@ function resolveInstalledSecrets(parentUrl) {
37656
37702
  let directory2 = dirname23(fileURLToPath3(parentUrl));
37657
37703
  for (;; ) {
37658
37704
  if (basename9(directory2) !== "node_modules") {
37659
- const candidate = join29(directory2, "node_modules", PACKAGE);
37705
+ const candidate = join30(directory2, "node_modules", PACKAGE);
37660
37706
  let absent = false;
37661
37707
  try {
37662
37708
  lstatSync24(candidate);
@@ -38963,7 +39009,7 @@ __export(exports_run, {
38963
39009
  });
38964
39010
  import { randomBytes as randomBytes2 } from "crypto";
38965
39011
  import { rmSync as rmSync2, writeFileSync as writeFileSync13 } from "fs";
38966
- import { basename as basename11, dirname as dirname25, isAbsolute as isAbsolute25, join as join30 } from "path";
39012
+ import { basename as basename11, dirname as dirname25, isAbsolute as isAbsolute25, join as join31 } from "path";
38967
39013
  function extensionOf(scriptPath) {
38968
39014
  const base = basename11(scriptPath);
38969
39015
  const dot = base.lastIndexOf(".");
@@ -39034,7 +39080,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
39034
39080
  async function executeVerifiedScript(options) {
39035
39081
  const scriptDir = dirname25(options.scriptPath);
39036
39082
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
39037
- const tempPath = join30(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
39083
+ const tempPath = join31(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
39038
39084
  let proc = null;
39039
39085
  let timedOut = false;
39040
39086
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -39126,10 +39172,10 @@ __export(exports_custom_install, {
39126
39172
  ensureCustomDir: () => ensureCustomDir
39127
39173
  });
39128
39174
  import { existsSync as existsSync15, mkdirSync as mkdirSync14, mkdtempSync, readFileSync as readFileSync23, rmSync as rmSync4 } from "fs";
39129
- import { join as join31 } from "path";
39175
+ import { join as join32 } from "path";
39130
39176
  import { tmpdir as tmpdir2 } from "os";
39131
39177
  function manifestAt(dir) {
39132
- const path = join31(dir, "manifest.json");
39178
+ const path = join32(dir, "manifest.json");
39133
39179
  if (!existsSync15(path))
39134
39180
  return;
39135
39181
  try {
@@ -39152,8 +39198,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
39152
39198
  return { name, version: manifest.version, kind, source, dir, scriptPath };
39153
39199
  }
39154
39200
  async function cloneToTemp(gitUrl) {
39155
- const tmp = mkdtempSync(join31(tmpdir2(), "hooks-install-"));
39156
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join31(tmp, "repo")], {
39201
+ const tmp = mkdtempSync(join32(tmpdir2(), "hooks-install-"));
39202
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join32(tmp, "repo")], {
39157
39203
  stdout: "pipe",
39158
39204
  stderr: "pipe"
39159
39205
  });
@@ -39166,7 +39212,7 @@ async function cloneToTemp(gitUrl) {
39166
39212
  rmSync4(tmp, { recursive: true, force: true });
39167
39213
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
39168
39214
  }
39169
- return join31(tmp, "repo");
39215
+ return join32(tmp, "repo");
39170
39216
  }
39171
39217
  function isGitUrl(value) {
39172
39218
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -40405,11 +40451,11 @@ __export(exports_serve, {
40405
40451
  DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
40406
40452
  });
40407
40453
  import { readFileSync as readFileSync26 } from "fs";
40408
- import { dirname as dirname26, join as join32 } from "path";
40454
+ import { dirname as dirname26, join as join33 } from "path";
40409
40455
  import { fileURLToPath as fileURLToPath4 } from "url";
40410
40456
  function packageVersion() {
40411
40457
  try {
40412
- const pkgPath = join32(dirname26(fileURLToPath4(import.meta.url)), "..", "package.json");
40458
+ const pkgPath = join33(dirname26(fileURLToPath4(import.meta.url)), "..", "package.json");
40413
40459
  return JSON.parse(readFileSync26(pkgPath, "utf-8")).version || "0.0.0";
40414
40460
  } catch {
40415
40461
  return "0.0.0";
@@ -40711,7 +40757,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
40711
40757
  import { z as z3 } from "zod";
40712
40758
  import { createServer } from "http";
40713
40759
  import { existsSync as existsSync16, readFileSync as readFileSync27 } from "fs";
40714
- import { join as join33, dirname as dirname27 } from "path";
40760
+ import { join as join34, dirname as dirname27 } from "path";
40715
40761
  import { fileURLToPath as fileURLToPath5 } from "url";
40716
40762
  async function runVerifiedHook(name, stdin, timeoutMs) {
40717
40763
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
@@ -40945,7 +40991,7 @@ function createHooksServer() {
40945
40991
  continue;
40946
40992
  }
40947
40993
  const hookDir = getHookPath(name);
40948
- if (!existsSync16(join33(hookDir, "src", "hook.ts"))) {
40994
+ if (!existsSync16(join34(hookDir, "src", "hook.ts"))) {
40949
40995
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
40950
40996
  hookHealthy = false;
40951
40997
  }
@@ -40989,7 +41035,7 @@ function createHooksServer() {
40989
41035
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
40990
41036
  }
40991
41037
  const hookPath = getHookPath(name);
40992
- const readmePath = join33(hookPath, "README.md");
41038
+ const readmePath = join34(hookPath, "README.md");
40993
41039
  let readme = "";
40994
41040
  if (existsSync16(readmePath)) {
40995
41041
  readme = readFileSync27(readmePath, "utf-8");
@@ -41719,7 +41765,7 @@ var init_server = __esm(() => {
41719
41765
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
41720
41766
  try {
41721
41767
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
41722
- const p = join33(__dirname3, rel);
41768
+ const p = join34(__dirname3, rel);
41723
41769
  if (existsSync16(p)) {
41724
41770
  pkg = JSON.parse(readFileSync27(p, "utf-8"));
41725
41771
  break;
@@ -41876,6 +41922,23 @@ function readNativeSafetyRegistration(home = homedir6()) {
41876
41922
  throw new NativeSafetyError(error61?.code === "ENOENT" ? "binding_missing" : "binding_invalid");
41877
41923
  }
41878
41924
  }
41925
+ function nativeSafetyBindingIntegrityReason(home = homedir6()) {
41926
+ try {
41927
+ const path = join12(canonicalHome(home), ".hasna/hooks/native", filename);
41928
+ const stat2 = lstatSync7(path);
41929
+ if (!stat2.isFile())
41930
+ return "the saved binding is not a regular file";
41931
+ if (!owner2(stat2.uid))
41932
+ return "the saved binding is not owned by the current user";
41933
+ if (stat2.mode & 63)
41934
+ return `the saved binding is accessible by group or others (mode ${(stat2.mode & 511).toString(8).padStart(3, "0")})`;
41935
+ if (stat2.size > 32768)
41936
+ return "the saved binding exceeds its size bound";
41937
+ return;
41938
+ } catch (error61) {
41939
+ return error61?.code === "ENOENT" ? "the saved binding is missing" : undefined;
41940
+ }
41941
+ }
41879
41942
  function registerNativeSafety(options = {}) {
41880
41943
  const home = canonicalHome(options.home ?? homedir6());
41881
41944
  const record3 = { schema: "hasna.hooks.native-safety.v1", harness: "sumi", command: options.command ?? installedNativeSafetyCommand("trash-guard") };
@@ -43205,17 +43268,17 @@ class JsonEventsStore {
43205
43268
  }
43206
43269
  async appendEvent(event) {
43207
43270
  await this.init();
43208
- const events = await this.readJson(this.eventsPath, []);
43209
- events.push(event);
43210
- await this.writeJson(this.eventsPath, events);
43271
+ const events2 = await this.readJson(this.eventsPath, []);
43272
+ events2.push(event);
43273
+ await this.writeJson(this.eventsPath, events2);
43211
43274
  return event;
43212
43275
  }
43213
43276
  async appendEventOnce(event, options = {}) {
43214
43277
  await this.init();
43215
- const events = await this.readJson(this.eventsPath, []);
43278
+ const events2 = await this.readJson(this.eventsPath, []);
43216
43279
  const dedupe = options.dedupe !== false;
43217
43280
  if (dedupe) {
43218
- const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
43281
+ const existing = findEventByIdentity(events2, { id: event.id, dedupeKey: event.dedupeKey });
43219
43282
  if (existing) {
43220
43283
  return {
43221
43284
  event: existing,
@@ -43225,8 +43288,8 @@ class JsonEventsStore {
43225
43288
  };
43226
43289
  }
43227
43290
  }
43228
- events.push(event);
43229
- await this.writeJson(this.eventsPath, events);
43291
+ events2.push(event);
43292
+ await this.writeJson(this.eventsPath, events2);
43230
43293
  return {
43231
43294
  event,
43232
43295
  stored: true,
@@ -43236,13 +43299,13 @@ class JsonEventsStore {
43236
43299
  }
43237
43300
  async listEvents(options = {}) {
43238
43301
  await this.init();
43239
- const events = await this.readJson(this.eventsPath, []);
43240
- return queryEvents(events, options);
43302
+ const events2 = await this.readJson(this.eventsPath, []);
43303
+ return queryEvents(events2, options);
43241
43304
  }
43242
43305
  async listEventsPage(options = {}) {
43243
43306
  await this.init();
43244
- const events = await this.readJson(this.eventsPath, []);
43245
- const queried = queryEvents(events, {
43307
+ const events2 = await this.readJson(this.eventsPath, []);
43308
+ const queried = queryEvents(events2, {
43246
43309
  eventId: options.eventId,
43247
43310
  source: options.source,
43248
43311
  type: options.type
@@ -43260,8 +43323,8 @@ class JsonEventsStore {
43260
43323
  };
43261
43324
  }
43262
43325
  async findEventByIdentity(identity3) {
43263
- const events = await this.listEvents();
43264
- return findEventByIdentity(events, identity3);
43326
+ const events2 = await this.listEvents();
43327
+ return findEventByIdentity(events2, identity3);
43265
43328
  }
43266
43329
  async appendDelivery(result) {
43267
43330
  await this.init();
@@ -43366,8 +43429,8 @@ function normalizeEventPageLimit(limit) {
43366
43429
  throw new Error(`Event page limit must be a positive integer, got ${limit}`);
43367
43430
  return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
43368
43431
  }
43369
- function queryEvents(events, options) {
43370
- let rows = events;
43432
+ function queryEvents(events2, options) {
43433
+ let rows = events2;
43371
43434
  if (options.eventId)
43372
43435
  rows = rows.filter((event) => event.id === options.eventId);
43373
43436
  if (options.source)
@@ -43386,13 +43449,13 @@ function assertCursorFilter(name, cursorValue, optionValue) {
43386
43449
  if (cursorValue !== optionValue)
43387
43450
  throw new Error(`Local JSON event cursor ${name} filter mismatch`);
43388
43451
  }
43389
- function findEventByIdentity(events, identity3) {
43390
- return events.find((event) => identity3.id !== undefined && event.id === identity3.id || identity3.dedupeKey !== undefined && event.dedupeKey === identity3.dedupeKey);
43452
+ function findEventByIdentity(events2, identity3) {
43453
+ return events2.find((event) => identity3.id !== undefined && event.id === identity3.id || identity3.dedupeKey !== undefined && event.dedupeKey === identity3.dedupeKey);
43391
43454
  }
43392
43455
  async function getEventsStatus(dataDir22) {
43393
43456
  const store = new JsonEventsStore(dataDir22);
43394
43457
  await store.init();
43395
- const [channels, events, deliveries] = await Promise.all([
43458
+ const [channels, events2, deliveries] = await Promise.all([
43396
43459
  store.listChannels(),
43397
43460
  store.listEvents(),
43398
43461
  store.listDeliveries()
@@ -43413,14 +43476,14 @@ async function getEventsStatus(dataDir22) {
43413
43476
  },
43414
43477
  files: {
43415
43478
  channels: statusFile(store.dataDir, "channels.json", channels.length),
43416
- events: statusFile(store.dataDir, "events.json", events.length),
43479
+ events: statusFile(store.dataDir, "events.json", events2.length),
43417
43480
  deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
43418
43481
  },
43419
43482
  counts: {
43420
43483
  channels: channels.length,
43421
43484
  enabledChannels: channels.filter((channel) => channel.enabled).length,
43422
43485
  disabledChannels: channels.filter((channel) => !channel.enabled).length,
43423
- events: events.length,
43486
+ events: events2.length,
43424
43487
  deliveries: deliveries.length
43425
43488
  },
43426
43489
  transports,
@@ -44135,16 +44198,16 @@ class EventsClient {
44135
44198
  async listEventsPage(options = {}) {
44136
44199
  if (this.store.listEventsPage)
44137
44200
  return this.store.listEventsPage(options);
44138
- const events = queryClientEvents(await this.store.listEvents(), {
44201
+ const events2 = queryClientEvents(await this.store.listEvents(), {
44139
44202
  eventId: options.eventId,
44140
44203
  source: options.source,
44141
44204
  type: options.type
44142
44205
  });
44143
44206
  const offset = decodeLocalJsonEventCursor(options.cursor, options);
44144
44207
  const limit = normalizeEventPageLimit(options.limit);
44145
- const pageEvents = events.slice(offset, offset + limit);
44208
+ const pageEvents = events2.slice(offset, offset + limit);
44146
44209
  const nextOffset = offset + pageEvents.length;
44147
- const hasMore = nextOffset < events.length;
44210
+ const hasMore = nextOffset < events2.length;
44148
44211
  return {
44149
44212
  events: pageEvents,
44150
44213
  cursor: options.cursor,
@@ -44287,8 +44350,8 @@ function sanitizeChannelForOutput(channel) {
44287
44350
  function sanitizeChannelsForOutput(channels) {
44288
44351
  return channels.map(sanitizeChannelForOutput);
44289
44352
  }
44290
- function queryClientEvents(events, options) {
44291
- let rows = events;
44353
+ function queryClientEvents(events2, options) {
44354
+ let rows = events2;
44292
44355
  if (options.eventId)
44293
44356
  rows = rows.filter((event) => event.id === options.eventId);
44294
44357
  if (options.source)
@@ -44521,8 +44584,8 @@ function registerChannelCommands(program2, options) {
44521
44584
  return channels;
44522
44585
  }
44523
44586
  function registerEventCommands(program2, options) {
44524
- const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
44525
- events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
44587
+ const events2 = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
44588
+ events2.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
44526
44589
  const result = await createClient(options).emit({
44527
44590
  source: actionOptions.source ?? options.source,
44528
44591
  type,
@@ -44536,7 +44599,7 @@ function registerEventCommands(program2, options) {
44536
44599
  print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
44537
44600
  });
44538
44601
  const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
44539
- events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
44602
+ events2.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
44540
44603
  let rows = await createClient(options).listEvents();
44541
44604
  if (actionOptions.source)
44542
44605
  rows = rows.filter((event) => event.source === actionOptions.source);
@@ -44555,7 +44618,7 @@ function registerEventCommands(program2, options) {
44555
44618
  for (const event of rows)
44556
44619
  console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
44557
44620
  });
44558
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
44621
+ events2.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
44559
44622
  const result = await createClient(options).replay({
44560
44623
  eventId: actionOptions.id,
44561
44624
  source: actionOptions.source,
@@ -44566,7 +44629,7 @@ function registerEventCommands(program2, options) {
44566
44629
  });
44567
44630
  print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
44568
44631
  });
44569
- return events;
44632
+ return events2;
44570
44633
  }
44571
44634
  function registerEventsCommands(program2, options) {
44572
44635
  registerChannelCommands(program2, options);
@@ -44582,9 +44645,9 @@ function collectValues(value, previous) {
44582
44645
  previous.push(value);
44583
44646
  return previous;
44584
44647
  }
44585
- function replaySummary(events, deliveries, nextCursor) {
44648
+ function replaySummary(events2, deliveries, nextCursor) {
44586
44649
  const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
44587
- return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
44650
+ return `Replayed ${events2} event(s), ${deliveries} delivery result(s)${suffix}`;
44588
44651
  }
44589
44652
 
44590
44653
  // src/cli/index.tsx
@@ -44609,7 +44672,7 @@ var {
44609
44672
  // src/cli/index.tsx
44610
44673
  import chalk2 from "chalk";
44611
44674
  import { existsSync as existsSync17, readFileSync as readFileSync28, rmSync as rmSync6 } from "fs";
44612
- import { join as join34, dirname as dirname28 } from "path";
44675
+ import { join as join35, dirname as dirname28 } from "path";
44613
44676
  import { fileURLToPath as fileURLToPath6 } from "url";
44614
44677
 
44615
44678
  // src/cli/components/App.tsx
@@ -45784,6 +45847,10 @@ function App({ initialHooks, overwrite = false }) {
45784
45847
  // src/cli/index.tsx
45785
45848
  init_native_safety();
45786
45849
 
45850
+ // src/lib/sumi-binding.ts
45851
+ import { homedir as homedir12 } from "os";
45852
+ import { join as join27, sep as sep3 } from "path";
45853
+
45787
45854
  // hooks/hook-trash-guard/src/binary.ts
45788
45855
  import { dirname as dirname22, isAbsolute as isAbsolute20, join as join24, resolve as resolve21 } from "path";
45789
45856
  import { accessSync, closeSync as closeSync17, constants as constants16, fstatSync as fstatSync15, lstatSync as lstatSync23, openSync as openSync17, readFileSync as readFileSync19, readlinkSync as readlinkSync3, readSync as readSync2, realpathSync as realpathSync12, statSync } from "fs";
@@ -45958,6 +46025,68 @@ function inspectTrashBinary(env = process.env) {
45958
46025
  return { ok: false, code: failure };
45959
46026
  }
45960
46027
 
46028
+ // src/lib/sumi-binding.ts
46029
+ init_native_safety();
46030
+ function failure(code2, reason) {
46031
+ return { ok: false, target: "sumi", code: code2, ...reason ? { reason: reason.slice(0, 300) } : {} };
46032
+ }
46033
+ function nativeSafetyBindingDurable(worker, home) {
46034
+ return !(worker.startsWith(join27(home, ".bun") + sep3) || worker.includes(`${sep3}node_modules${sep3}.bun${sep3}`));
46035
+ }
46036
+ async function provisionSumiSafetyBinding(options = {}) {
46037
+ const home = options.home ?? homedir12();
46038
+ let desired;
46039
+ try {
46040
+ desired = options.command ?? installedNativeSafetyCommand("trash-guard");
46041
+ } catch (error61) {
46042
+ const message = error61 instanceof Error ? error61.message : String(error61);
46043
+ return failure(/Bundled native safety runner is missing/.test(message) ? "runner_missing" : "guard_failed", message);
46044
+ }
46045
+ try {
46046
+ if (nativeSafetyCommandBinding(desired)?.name !== "trash-guard")
46047
+ throw new NativeSafetyError("input_invalid");
46048
+ const durable = nativeSafetyBindingDurable(nativeSafetyCommandBinding(desired).worker, home);
46049
+ let current2;
46050
+ try {
46051
+ current2 = readNativeSafetyRegistration(home);
46052
+ } catch (error61) {
46053
+ if (!(error61 instanceof NativeSafetyError) || error61.code !== "binding_missing")
46054
+ throw error61;
46055
+ }
46056
+ if (current2) {
46057
+ if (current2.record.command === desired) {
46058
+ return { ok: true, target: "sumi", action: "unchanged", path: current2.path, sha256: current2.sha256, changed: false, durable };
46059
+ }
46060
+ if (nativeSafetyBindingVerified(current2.record.command)) {
46061
+ return { ok: true, target: "sumi", action: "kept", path: current2.path, sha256: current2.sha256, changed: false, durable };
46062
+ }
46063
+ }
46064
+ const binding = nativeSafetyCommandBinding(desired);
46065
+ const trash = inspectTrashBinary({ PATH: binding.path });
46066
+ if (!trash.ok)
46067
+ return failure(trash.code);
46068
+ await verifyNativeSafetyCommand(desired, home);
46069
+ const registered = registerNativeSafety({ home, command: desired, expectedSHA256: current2?.sha256 });
46070
+ const verified = await verifyNativeSafetyRegistration(home);
46071
+ if (verified.sha256 !== registered.sha256)
46072
+ throw new NativeSafetyError("binding_changed");
46073
+ return {
46074
+ ok: true,
46075
+ target: "sumi",
46076
+ action: current2 ? "repaired" : "created",
46077
+ path: registered.path,
46078
+ sha256: registered.sha256,
46079
+ changed: true,
46080
+ durable,
46081
+ ...current2 ? { backup: `${registered.path}.before-${current2.sha256}` } : {}
46082
+ };
46083
+ } catch (error61) {
46084
+ if (!(error61 instanceof NativeSafetyError))
46085
+ return failure("guard_failed", error61 instanceof Error ? error61.message : String(error61));
46086
+ return failure(error61.code, error61.reason ?? (error61.code === "binding_invalid" ? nativeSafetyBindingIntegrityReason(home) : undefined));
46087
+ }
46088
+ }
46089
+
45961
46090
  // src/cli/index.tsx
45962
46091
  init_registry();
45963
46092
  init_installer();
@@ -45975,7 +46104,7 @@ init_transport2();
45975
46104
  init_db();
45976
46105
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
45977
46106
  var __dirname4 = dirname28(fileURLToPath6(import.meta.url));
45978
- var pkgPath = existsSync17(join34(__dirname4, "..", "package.json")) ? join34(__dirname4, "..", "package.json") : join34(__dirname4, "..", "..", "package.json");
46107
+ var pkgPath = existsSync17(join35(__dirname4, "..", "package.json")) ? join35(__dirname4, "..", "package.json") : join35(__dirname4, "..", "..", "package.json");
45979
46108
  var pkg2 = JSON.parse(readFileSync28(pkgPath, "utf-8"));
45980
46109
  var program3 = new Command;
45981
46110
  function resolveScope(options) {
@@ -46003,6 +46132,20 @@ function resolveConcreteTarget(options) {
46003
46132
  return "codewith";
46004
46133
  return "claude";
46005
46134
  }
46135
+ function reportSumiBinding(result) {
46136
+ if (result.ok) {
46137
+ if (result.changed) {
46138
+ console.log(chalk2.green(`\u2713 Sumi native-safety binding ${result.action}`) + chalk2.dim(` (${result.path})`));
46139
+ if (result.backup)
46140
+ console.log(chalk2.dim(` previous binding preserved at ${result.backup}`));
46141
+ }
46142
+ if (!result.durable) {
46143
+ console.error(chalk2.yellow("\u26A0 The Sumi binding pins a worker in a tree a package upgrade rewrites") + chalk2.dim(" \u2014 re-run `hooks safety provision` after upgrading that tree, or provision from a stable versioned install tree."));
46144
+ }
46145
+ return;
46146
+ }
46147
+ console.error(chalk2.yellow(`\u26A0 Sumi native-safety binding was not provisioned (${result.code})`) + chalk2.dim(` \u2014 run \`hooks safety provision\` on this station before a Sumi seat needs its shell.`));
46148
+ }
46006
46149
  function formatSettingsPath(scope, target) {
46007
46150
  if (target === "all")
46008
46151
  return "target-specific settings";
@@ -46194,7 +46337,7 @@ program3.command("init").description("Register a new agent profile with a unique
46194
46337
  if (profile.name) {
46195
46338
  console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
46196
46339
  }
46197
- console.log(` ${chalk2.dim("Profile:")} ${join34(getProfilesDir(), `${profile.agent_id}.json`)}`);
46340
+ console.log(` ${chalk2.dim("Profile:")} ${join35(getProfilesDir(), `${profile.agent_id}.json`)}`);
46198
46341
  console.log();
46199
46342
  console.log(chalk2.dim(" Install hooks with this profile:"));
46200
46343
  console.log(` hooks install gitguard --profile ${profile.agent_id}`);
@@ -46512,6 +46655,7 @@ Dry run \u2014 would install (${scope}, ${target}):
46512
46655
  });
46513
46656
  results2.push(result);
46514
46657
  }
46658
+ const sumiBinding = await provisionSumiSafetyBinding();
46515
46659
  if (options.json) {
46516
46660
  console.log(JSON.stringify({
46517
46661
  installed: results2.filter((r2) => r2.success).map((r2) => r2.hook),
@@ -46522,6 +46666,7 @@ Dry run \u2014 would install (${scope}, ${target}):
46522
46666
  scope,
46523
46667
  target,
46524
46668
  applied: results2.some((r2) => r2.applied),
46669
+ sumiBinding,
46525
46670
  ...target === "codex" ? { notes: results2.filter((r2) => r2.success && r2.note).map((r2) => r2.note) } : {}
46526
46671
  }));
46527
46672
  if (results2.some((r2) => !r2.success)) {
@@ -46571,6 +46716,7 @@ Installing hooks (${scope}, ${target})...
46571
46716
  }
46572
46717
  console.log(chalk2.dim(`
46573
46718
  Registered in ${settingsFile}`));
46719
+ reportSumiBinding(sumiBinding);
46574
46720
  });
46575
46721
  program3.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
46576
46722
  const scope = resolveScope(options);
@@ -46846,7 +46992,7 @@ program3.command("doctor").option("-g, --global", "Check global settings", false
46846
46992
  continue;
46847
46993
  }
46848
46994
  const hookDir = getHookPath(name);
46849
- const hookScript = join34(hookDir, "src", "hook.ts");
46995
+ const hookScript = join35(hookDir, "src", "hook.ts");
46850
46996
  if (!existsSync17(hookScript)) {
46851
46997
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
46852
46998
  hookHealthy = false;
@@ -47057,7 +47203,7 @@ program3.command("docs").argument("[hook]", "Hook name (shows general docs if om
47057
47203
  return;
47058
47204
  }
47059
47205
  const hookPath = getHookPath(hook);
47060
- const readmePath = join34(hookPath, "README.md");
47206
+ const readmePath = join35(hookPath, "README.md");
47061
47207
  let readme = "";
47062
47208
  if (existsSync17(readmePath)) {
47063
47209
  readme = readFileSync28(readmePath, "utf-8");
@@ -47233,11 +47379,13 @@ Upgrading @hasna/hooks (${pm})...
47233
47379
  }
47234
47380
  const versionProc = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
47235
47381
  const latest = new TextDecoder().decode(versionProc.stdout).trim() || "unknown";
47382
+ const sumiBinding = await provisionSumiSafetyBinding();
47236
47383
  if (options.json) {
47237
- console.log(JSON.stringify({ current: current2, latest, updated: true }));
47384
+ console.log(JSON.stringify({ current: current2, latest, updated: true, sumiBinding }));
47238
47385
  } else {
47239
47386
  console.log(chalk2.green(`
47240
47387
  \u2713 Upgraded: ${current2} \u2192 ${latest}`));
47388
+ reportSumiBinding(sumiBinding);
47241
47389
  }
47242
47390
  });
47243
47391
  program3.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
@@ -47810,6 +47958,17 @@ safety.command("verify").requiredOption("--target <target>", "sumi, codex or cla
47810
47958
  process.exitCode = 1;
47811
47959
  }
47812
47960
  });
47961
+ safety.command("provision").option("--target <target>", "sumi", "sumi").description("Idempotently provision the required native binding from this installed package").action(async (options) => {
47962
+ if (options.target !== "sumi") {
47963
+ console.log(JSON.stringify({ ok: false, target: options.target, code: "input_invalid" }));
47964
+ process.exitCode = 1;
47965
+ return;
47966
+ }
47967
+ const result = await provisionSumiSafetyBinding();
47968
+ console.log(JSON.stringify(result));
47969
+ if (!result.ok)
47970
+ process.exitCode = 1;
47971
+ });
47813
47972
  var API_INDEPENDENT_COMMANDS = new Set([
47814
47973
  "safety",
47815
47974
  "serve",