@hasna/hooks 0.9.1 → 0.9.3

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/hooks-mcp.js CHANGED
@@ -377,6 +377,17 @@ var init_registry = __esm(() => {
377
377
  matcher: "",
378
378
  tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
379
379
  },
380
+ {
381
+ name: "mementos-context",
382
+ displayName: "Mementos Prompt Context",
383
+ description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
384
+ version: "0.1.0",
385
+ category: "Context Management",
386
+ event: "UserPromptSubmit",
387
+ matcher: "",
388
+ timeoutSeconds: 12,
389
+ tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
390
+ },
380
391
  {
381
392
  name: "autoformat",
382
393
  displayName: "Auto Format",
@@ -1410,7 +1421,7 @@ var init_redact = __esm(() => {
1410
1421
  // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
1411
1422
  import { isIP } from "net";
1412
1423
  import { spawnSync } from "child_process";
1413
- import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
1424
+ import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
1414
1425
  import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
1415
1426
  import { createRequire } from "module";
1416
1427
  import { hostname as osHostname } from "os";
@@ -1514,7 +1525,7 @@ function readAppConfigFile(path) {
1514
1525
  };
1515
1526
  let fd = -1;
1516
1527
  try {
1517
- fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
1528
+ fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
1518
1529
  } catch (error) {
1519
1530
  const code = error.code;
1520
1531
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -1535,7 +1546,7 @@ function readAppConfigFile(path) {
1535
1546
  unsafe("the file is not owned by the current user");
1536
1547
  if (before.size > MAX_CREDENTIAL_FILE_BYTES)
1537
1548
  unsafe("the file exceeds the size limit");
1538
- const bytes = readFileSync6(fd);
1549
+ const bytes = readFileSync7(fd);
1539
1550
  const after = fstatSync(fd);
1540
1551
  if (!configFileReadsCoherent(before, after)) {
1541
1552
  unsafe("the file changed while being read");
@@ -1543,7 +1554,7 @@ function readAppConfigFile(path) {
1543
1554
  return parseEnvFile(bytes.toString("utf8"));
1544
1555
  } finally {
1545
1556
  if (fd !== -1)
1546
- closeSync(fd);
1557
+ closeSync2(fd);
1547
1558
  }
1548
1559
  }
1549
1560
  function readCredentialFile(path, apiKeyKeys) {
@@ -2198,8 +2209,8 @@ var init_local_opt_in = __esm(() => {
2198
2209
  });
2199
2210
 
2200
2211
  // src/lib/hook-env.ts
2201
- import { dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
2202
- import { existsSync as existsSync9, realpathSync as realpathSync2, statSync } from "fs";
2212
+ import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
2213
+ import { existsSync as existsSync10, realpathSync as realpathSync2, statSync } from "fs";
2203
2214
  import { tmpdir } from "os";
2204
2215
  function isDeniedEnvName(name) {
2205
2216
  const upper = name.toUpperCase();
@@ -2247,9 +2258,9 @@ function isUnsafePathEntry(entry, home) {
2247
2258
  }
2248
2259
  function sanitizeHookPath(pathValue, home) {
2249
2260
  const baseline = PATH_BASELINE.slice();
2250
- if (process.platform === "darwin" && existsSync9("/opt/homebrew/bin"))
2261
+ if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
2251
2262
  baseline.push("/opt/homebrew/bin");
2252
- const bunDir = dirname4(process.execPath);
2263
+ const bunDir = dirname5(process.execPath);
2253
2264
  const seen = new Set;
2254
2265
  const out = [];
2255
2266
  for (const dir of baseline) {
@@ -2373,8 +2384,8 @@ __export(exports_run, {
2373
2384
  HookTimeoutError: () => HookTimeoutError
2374
2385
  });
2375
2386
  import { randomBytes as randomBytes2 } from "crypto";
2376
- import { rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
2377
- import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join10 } from "path";
2387
+ import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
2388
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join10 } from "path";
2378
2389
  function extensionOf(scriptPath) {
2379
2390
  const base = basename2(scriptPath);
2380
2391
  const dot = base.lastIndexOf(".");
@@ -2443,7 +2454,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
2443
2454
  return chunks.join("");
2444
2455
  }
2445
2456
  async function executeVerifiedScript(options) {
2446
- const scriptDir = dirname5(options.scriptPath);
2457
+ const scriptDir = dirname6(options.scriptPath);
2447
2458
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
2448
2459
  const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
2449
2460
  let proc = null;
@@ -2453,7 +2464,7 @@ async function executeVerifiedScript(options) {
2453
2464
  killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
2454
2465
  }, options.timeout) : null;
2455
2466
  try {
2456
- writeFileSync5(tempPath, options.content, { flag: "wx", mode: 384 });
2467
+ writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
2457
2468
  proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
2458
2469
  stdin: new Response(options.stdin),
2459
2470
  stdout: "pipe",
@@ -2653,21 +2664,94 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2653
2664
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2654
2665
  import { z as z2 } from "zod";
2655
2666
  init_registry();
2656
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
2657
- import { join as join11, dirname as dirname6 } from "path";
2667
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
2668
+ import { join as join11, dirname as dirname7 } from "path";
2658
2669
  import { fileURLToPath as fileURLToPath3 } from "url";
2659
2670
 
2660
2671
  // src/lib/installer.ts
2661
2672
  init_registry();
2662
2673
  init_resolve();
2663
2674
  init_manifest();
2664
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2665
- import { join as join7, dirname as dirname3 } from "path";
2675
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2676
+ import { join as join7, dirname as dirname4 } from "path";
2666
2677
  import { homedir as homedir3 } from "os";
2667
2678
  import { fileURLToPath as fileURLToPath2 } from "url";
2668
2679
 
2669
2680
  // src/lib/registration.ts
2670
2681
  init_resolve();
2682
+
2683
+ // src/lib/mementos-options.ts
2684
+ function validateMementosRegistration(name, value) {
2685
+ if (value === undefined)
2686
+ return;
2687
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || !Object.hasOwn(value, "enabled") || Object.keys(value).some((key) => key !== "enabled" && key !== "secretRef") || name.replace(/^hook-/, "") !== "mementos-context" || typeof value.enabled !== "boolean") {
2688
+ throw new Error("Mementos options require the mementos-context hook and an explicit enable/disable choice.");
2689
+ }
2690
+ if (value.secretRef !== undefined && (!value.enabled || typeof value.secretRef !== "string" || value.secretRef.length > 256 || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*(?:\/[a-zA-Z0-9][a-zA-Z0-9_.-]*){1,12}$/.test(value.secretRef))) {
2691
+ throw new Error("OpenRouter requires an enabled Mementos hook and a valid Secrets reference, never a key value.");
2692
+ }
2693
+ }
2694
+ function mementosRegistrationFromFlags(name, flags) {
2695
+ if (!flags.enableMementos && !flags.disableMementos && flags.openrouterSecretRef === undefined)
2696
+ return;
2697
+ if (Boolean(flags.enableMementos) === Boolean(flags.disableMementos)) {
2698
+ throw new Error("Choose exactly one of --enable-mementos or --disable-mementos.");
2699
+ }
2700
+ const value = { enabled: flags.enableMementos === true, ...flags.openrouterSecretRef !== undefined ? { secretRef: flags.openrouterSecretRef } : {} };
2701
+ validateMementosRegistration(name, value);
2702
+ return value;
2703
+ }
2704
+ function mementosCommandSuffix(value) {
2705
+ validateMementosRegistration("mementos-context", value);
2706
+ if (!value)
2707
+ return "";
2708
+ return value.enabled ? ` --enable-mementos${value.secretRef ? ` --openrouter-secret-ref ${value.secretRef}` : ""}` : " --disable-mementos";
2709
+ }
2710
+ function retainedMementosRegistration(commands) {
2711
+ const values = commands.filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
2712
+ const args = command.trim().split(/\s+/).slice(3);
2713
+ const flags = {};
2714
+ const seen = new Set;
2715
+ while (args.length) {
2716
+ const key = args.shift();
2717
+ if (seen.has(key))
2718
+ throw new Error("Ambiguous Mementos registration; specify its enablement explicitly.");
2719
+ seen.add(key);
2720
+ if (key === "--profile" && /^[\w-]+$/.test(args[0] ?? ""))
2721
+ args.shift();
2722
+ else if (key === "--enable-mementos")
2723
+ flags.enableMementos = true;
2724
+ else if (key === "--disable-mementos")
2725
+ flags.disableMementos = true;
2726
+ else if (key === "--openrouter-secret-ref" && args.length)
2727
+ flags.openrouterSecretRef = args.shift();
2728
+ else
2729
+ throw new Error("Unrecognized Mementos registration options; specify its enablement explicitly.");
2730
+ }
2731
+ return mementosRegistrationFromFlags("mementos-context", flags);
2732
+ });
2733
+ if (new Set(values.map((value) => JSON.stringify(value))).size > 1) {
2734
+ throw new Error("Conflicting Mementos registrations; specify its enablement explicitly.");
2735
+ }
2736
+ return values[0];
2737
+ }
2738
+
2739
+ // src/lib/registration.ts
2740
+ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|hook-([\w-]+))$/;
2741
+ function registrationName(command) {
2742
+ if (typeof command !== "string")
2743
+ return;
2744
+ if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
2745
+ try {
2746
+ retainedMementosRegistration([command.trim()]);
2747
+ return "mementos-context";
2748
+ } catch {
2749
+ return;
2750
+ }
2751
+ }
2752
+ const match = command.trim().match(REGISTRATION_COMMAND_RE);
2753
+ return match ? match[1] ?? match[2] : undefined;
2754
+ }
2671
2755
  function matchersOverlap(a, b) {
2672
2756
  return a === b || a.includes(b) || b.includes(a);
2673
2757
  }
@@ -2861,9 +2945,78 @@ function checkScriptHash(name, actual) {
2861
2945
  return { ok: true, pinned: true, expected, actual, name };
2862
2946
  }
2863
2947
 
2948
+ // src/lib/codex-settings.ts
2949
+ import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2950
+ import { dirname as dirname3 } from "path";
2951
+ import { randomUUID } from "crypto";
2952
+ var snapshots = new WeakMap;
2953
+ function current(path) {
2954
+ try {
2955
+ const stat = lstatSync(path);
2956
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
2957
+ throw new Error("invalid_file");
2958
+ return readFileSync4(path, "utf8");
2959
+ } catch (error) {
2960
+ if (error.code === "ENOENT")
2961
+ return null;
2962
+ throw new Error("Cannot safely read Codex hooks.json; no settings were changed.");
2963
+ }
2964
+ }
2965
+ function readCodexSettings(path) {
2966
+ const raw = current(path);
2967
+ let settings;
2968
+ try {
2969
+ settings = raw === null ? {} : JSON.parse(raw);
2970
+ if (!settings || typeof settings !== "object" || Array.isArray(settings))
2971
+ throw new Error;
2972
+ if (settings.hooks !== undefined) {
2973
+ if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks))
2974
+ throw new Error;
2975
+ for (const entries of Object.values(settings.hooks)) {
2976
+ if (!Array.isArray(entries) || entries.some((entry) => !entry || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !hook || typeof hook !== "object")))
2977
+ throw new Error;
2978
+ }
2979
+ }
2980
+ } catch {
2981
+ throw new Error("Invalid Codex hooks.json; repair it before installing or removing hooks.");
2982
+ }
2983
+ snapshots.set(settings, { path, raw });
2984
+ return settings;
2985
+ }
2986
+ function writeCodexSettings(path, settings) {
2987
+ const snapshot = snapshots.get(settings);
2988
+ if (!snapshot || snapshot.path !== path)
2989
+ throw new Error("Codex settings require a complete current read before writing.");
2990
+ mkdirSync4(dirname3(path), { recursive: true });
2991
+ const lock = `${path}.hooks-write.lock`;
2992
+ let fd;
2993
+ try {
2994
+ fd = openSync(lock, "wx", 384);
2995
+ } catch {
2996
+ throw new Error("Codex settings are being edited; retry after the active writer finishes.");
2997
+ }
2998
+ const temp = `${path}.${randomUUID()}.tmp`;
2999
+ try {
3000
+ if (current(path) !== snapshot.raw)
3001
+ throw new Error("Codex settings changed since reading; re-read before retrying.");
3002
+ const next = JSON.stringify(settings, null, 2) + `
3003
+ `;
3004
+ writeFileSync3(temp, next, { flag: "wx", mode: 384 });
3005
+ if (current(path) !== snapshot.raw)
3006
+ throw new Error("Codex settings changed since reading; re-read before retrying.");
3007
+ renameSync2(temp, path);
3008
+ snapshots.set(settings, { path, raw: next });
3009
+ } finally {
3010
+ if (existsSync7(temp))
3011
+ unlinkSync(temp);
3012
+ closeSync(fd);
3013
+ unlinkSync(lock);
3014
+ }
3015
+ }
3016
+
2864
3017
  // src/lib/installer.ts
2865
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2866
- var HOOKS_DIR = existsSync7(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
3018
+ var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
3019
+ var HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
2867
3020
  function normalizeHookName(name) {
2868
3021
  return name.startsWith("hook-") ? name : `hook-${name}`;
2869
3022
  }
@@ -2871,10 +3024,15 @@ function shortHookName(name) {
2871
3024
  return name.startsWith("hook-") ? name.slice("hook-".length) : name;
2872
3025
  }
2873
3026
  function removeHookEntriesByName(entries, hookName) {
2874
- return entries.filter((entry) => !entry.hooks?.some((h) => {
2875
- const match = h.command?.match(/^hooks run ([\w-]+)/);
2876
- return match && match[1] === hookName;
2877
- }));
3027
+ return entries.flatMap((entry) => {
3028
+ if (!Array.isArray(entry.hooks))
3029
+ return [entry];
3030
+ const kept = entry.hooks.filter((h) => {
3031
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
3032
+ return !(match && match[1] === hookName);
3033
+ });
3034
+ return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
3035
+ });
2878
3036
  }
2879
3037
  var EVENT_MAP = {
2880
3038
  claude: {
@@ -2884,7 +3042,7 @@ var EVENT_MAP = {
2884
3042
  Notification: "Notification",
2885
3043
  SessionStart: "SessionStart",
2886
3044
  SessionEnd: "SessionEnd",
2887
- UserPromptSubmit: null,
3045
+ UserPromptSubmit: "UserPromptSubmit",
2888
3046
  SubagentStart: null
2889
3047
  },
2890
3048
  gemini: {
@@ -2906,9 +3064,21 @@ var EVENT_MAP = {
2906
3064
  SessionEnd: null,
2907
3065
  UserPromptSubmit: "UserPromptSubmit",
2908
3066
  SubagentStart: "SubagentStart"
3067
+ },
3068
+ codex: {
3069
+ PreToolUse: "PreToolUse",
3070
+ PostToolUse: "PostToolUse",
3071
+ Stop: "Stop",
3072
+ Notification: null,
3073
+ SessionStart: "SessionStart",
3074
+ SessionEnd: "SessionEnd",
3075
+ UserPromptSubmit: "UserPromptSubmit",
3076
+ SubagentStart: "SubagentStart"
2909
3077
  }
2910
3078
  };
2911
3079
  function getTargetSettingsDir(target) {
3080
+ if (target === "codex")
3081
+ return ".codex";
2912
3082
  if (target === "codewith")
2913
3083
  return ".codewith";
2914
3084
  if (target === "gemini")
@@ -2916,6 +3086,8 @@ function getTargetSettingsDir(target) {
2916
3086
  return ".claude";
2917
3087
  }
2918
3088
  function getGlobalSettingsPathOverride(target) {
3089
+ if (target === "codex")
3090
+ return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
2919
3091
  if (target === "claude")
2920
3092
  return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
2921
3093
  if (target === "gemini")
@@ -2932,6 +3104,8 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
2932
3104
  if (globalOverride)
2933
3105
  return globalOverride;
2934
3106
  const dir = getTargetSettingsDir(target);
3107
+ if (target === "codex")
3108
+ return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
2935
3109
  if (scope === "project") {
2936
3110
  return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
2937
3111
  }
@@ -2945,13 +3119,15 @@ function getHookPath(name) {
2945
3119
  return join7(HOOKS_DIR, normalizeHookName(shortName));
2946
3120
  }
2947
3121
  function hookExists(name) {
2948
- return existsSync7(getHookPath(name));
3122
+ return existsSync8(getHookPath(name));
2949
3123
  }
2950
3124
  function readSettings(scope = "global", target = "claude") {
2951
3125
  const path = getSettingsPath(scope, target);
3126
+ if (target === "codex")
3127
+ return readCodexSettings(path);
2952
3128
  try {
2953
- if (existsSync7(path)) {
2954
- return JSON.parse(readFileSync4(path, "utf-8"));
3129
+ if (existsSync8(path)) {
3130
+ return JSON.parse(readFileSync5(path, "utf-8"));
2955
3131
  }
2956
3132
  } catch (error) {
2957
3133
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -2960,11 +3136,13 @@ function readSettings(scope = "global", target = "claude") {
2960
3136
  }
2961
3137
  function writeSettings(settings, scope = "global", target = "claude") {
2962
3138
  const path = getSettingsPath(scope, target);
2963
- const dir = dirname3(path);
2964
- if (!existsSync7(dir)) {
2965
- mkdirSync4(dir, { recursive: true });
3139
+ if (target === "codex")
3140
+ return writeCodexSettings(path, settings);
3141
+ const dir = dirname4(path);
3142
+ if (!existsSync8(dir)) {
3143
+ mkdirSync5(dir, { recursive: true });
2966
3144
  }
2967
- writeFileSync3(path, JSON.stringify(settings, null, 2) + `
3145
+ writeFileSync4(path, JSON.stringify(settings, null, 2) + `
2968
3146
  `);
2969
3147
  }
2970
3148
  function getTargetEventName(internalEvent, target) {
@@ -2987,6 +3165,8 @@ function codewithMatcher(matcher) {
2987
3165
  }
2988
3166
  function codewithTimeout(name) {
2989
3167
  switch (name) {
3168
+ case "mementos-context":
3169
+ return 12;
2990
3170
  case "knowledge-context":
2991
3171
  return 6;
2992
3172
  case "session-start":
@@ -3023,12 +3203,13 @@ function codewithStatusMessage(name) {
3023
3203
  return `Running ${name}`;
3024
3204
  }
3025
3205
  }
3026
- function buildCodewithTomlFragment(name, profile) {
3206
+ function buildCodewithTomlFragment(name, profile, mementos) {
3027
3207
  const shortName = shortHookName(name);
3028
3208
  const meta = resolveHookMeta(shortName);
3029
3209
  if (!meta)
3030
3210
  throw new Error(`Hook '${shortName}' not found`);
3031
- const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
3211
+ validateMementosRegistration(shortName, mementos);
3212
+ const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
3032
3213
  const matcher = codewithMatcher(meta.matcher);
3033
3214
  const fragments = [];
3034
3215
  for (const event of getHookEvents(meta)) {
@@ -3053,21 +3234,21 @@ function buildCodewithTomlFragment(name, profile) {
3053
3234
  function readCodewithConfig(scope, configPath) {
3054
3235
  const path = getSettingsPath(scope, "codewith", configPath);
3055
3236
  try {
3056
- return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
3237
+ return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
3057
3238
  } catch {
3058
3239
  return "";
3059
3240
  }
3060
3241
  }
3061
3242
  function appendCodewithFragment(fragment, scope, configPath) {
3062
3243
  const path = getSettingsPath(scope, "codewith", configPath);
3063
- const dir = dirname3(path);
3064
- if (!existsSync7(dir))
3065
- mkdirSync4(dir, { recursive: true });
3066
- const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
3244
+ const dir = dirname4(path);
3245
+ if (!existsSync8(dir))
3246
+ mkdirSync5(dir, { recursive: true });
3247
+ const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
3067
3248
  const sep2 = existing.trim() ? `
3068
3249
 
3069
3250
  ` : "";
3070
- writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
3251
+ writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
3071
3252
  return path;
3072
3253
  }
3073
3254
  function detectConflict(name, scope, target) {
@@ -3110,14 +3291,14 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
3110
3291
  }
3111
3292
  return;
3112
3293
  }
3113
- function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
3294
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
3114
3295
  const shortName = shortHookName(name);
3115
3296
  if (!hookExists(shortName)) {
3116
3297
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
3117
3298
  }
3118
3299
  if (target === "codewith") {
3119
3300
  try {
3120
- const fragment = buildCodewithTomlFragment(shortName, profile);
3301
+ const fragment = buildCodewithTomlFragment(shortName, profile, mementos);
3121
3302
  if (codewithMode !== "write") {
3122
3303
  return {
3123
3304
  hook: shortName,
@@ -3164,18 +3345,25 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
3164
3345
  };
3165
3346
  }
3166
3347
  }
3167
- const registered = getRegisteredHooksForTarget(scope, target);
3168
- if (registered.includes(shortName) && !overwrite) {
3169
- return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
3170
- }
3171
- const conflict = detectConflict(shortName, scope, target);
3172
- const rewriteConflict = detectRewriteConflict(shortName, scope, target);
3173
- if (rewriteConflict) {
3174
- return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
3175
- }
3176
3348
  try {
3177
- registerHook(shortName, scope, target, profile);
3178
- return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
3349
+ const registered = getRegisteredHooksForTarget(scope, target);
3350
+ if (registered.includes(shortName) && !overwrite) {
3351
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
3352
+ }
3353
+ const conflict = detectConflict(shortName, scope, target);
3354
+ const rewriteConflict = detectRewriteConflict(shortName, scope, target);
3355
+ if (rewriteConflict) {
3356
+ return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
3357
+ }
3358
+ registerHook(shortName, scope, target, profile, mementos);
3359
+ return {
3360
+ hook: shortName,
3361
+ success: true,
3362
+ scope,
3363
+ target,
3364
+ ...conflict ? { conflict } : {},
3365
+ ...target === "codex" ? { applied: true, configPath: getSettingsPath(scope, target), note: "Registered in Codex hooks.json. Review and trust the exact definition in Codex /hooks; project hooks also require a trusted project. Registration alone does not prove execution." } : {}
3366
+ };
3179
3367
  } catch (error) {
3180
3368
  return {
3181
3369
  hook: shortName,
@@ -3186,7 +3374,15 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
3186
3374
  }
3187
3375
  }
3188
3376
  function installHook(name, options = {}) {
3189
- const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
3377
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
3378
+ try {
3379
+ validateMementosRegistration(name, mementos);
3380
+ } catch (error) {
3381
+ return { hook: name, success: false, target, error: error instanceof Error ? error.message : "Invalid Mementos options" };
3382
+ }
3383
+ if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
3384
+ return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
3385
+ }
3190
3386
  if (target === "all") {
3191
3387
  const shortName = shortHookName(name);
3192
3388
  const meta = resolveHookMeta(shortName);
@@ -3201,7 +3397,7 @@ function installHook(name, options = {}) {
3201
3397
  };
3202
3398
  }
3203
3399
  }
3204
- const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
3400
+ const claudeResult = installForTarget(name, scope, overwrite, "claude", profile, "fragment", undefined, mementos);
3205
3401
  if (!claudeResult.success) {
3206
3402
  return {
3207
3403
  ...claudeResult,
@@ -3209,7 +3405,7 @@ function installHook(name, options = {}) {
3209
3405
  target: "all"
3210
3406
  };
3211
3407
  }
3212
- const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
3408
+ const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath, mementos);
3213
3409
  if (!codewithResult.success) {
3214
3410
  return {
3215
3411
  ...codewithResult,
@@ -3219,9 +3415,9 @@ function installHook(name, options = {}) {
3219
3415
  }
3220
3416
  return { ...claudeResult, target: "all" };
3221
3417
  }
3222
- return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
3418
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
3223
3419
  }
3224
- function registerHook(name, scope = "global", target = "claude", profile) {
3420
+ function registerHook(name, scope = "global", target = "claude", profile, mementos) {
3225
3421
  const meta = resolveHookMeta(name);
3226
3422
  if (!meta)
3227
3423
  return;
@@ -3239,8 +3435,9 @@ function registerHook(name, scope = "global", target = "claude", profile) {
3239
3435
  const settings = readSettings(scope, target);
3240
3436
  if (!settings.hooks)
3241
3437
  settings.hooks = {};
3438
+ const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
3242
3439
  removeHookFromAllEvents(settings, name);
3243
- const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
3440
+ const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
3244
3441
  for (const eventKey of uniqueEventKeys) {
3245
3442
  if (!settings.hooks[eventKey])
3246
3443
  settings.hooks[eventKey] = [];
@@ -3282,10 +3479,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3282
3479
  if (target === "codewith") {
3283
3480
  const config = readCodewithConfig(scope);
3284
3481
  const registered2 = [];
3285
- const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
3482
+ const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
3286
3483
  let match;
3287
3484
  while (match = re.exec(config)) {
3288
- registered2.push(match[1]);
3485
+ const name = registrationName(match[1]);
3486
+ if (name)
3487
+ registered2.push(name);
3289
3488
  }
3290
3489
  return [...new Set(registered2)];
3291
3490
  }
@@ -3296,12 +3495,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3296
3495
  for (const eventKey of Object.keys(settings.hooks)) {
3297
3496
  for (const entry of settings.hooks[eventKey]) {
3298
3497
  for (const hook of entry.hooks || []) {
3299
- const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
3300
- const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
3301
- const match = newMatch || oldMatch;
3302
- if (match) {
3303
- registered.push(match[1]);
3304
- }
3498
+ const name = registrationName(hook.command);
3499
+ if (name)
3500
+ registered.push(name);
3305
3501
  }
3306
3502
  }
3307
3503
  }
@@ -3315,7 +3511,8 @@ function removeHook(name, scope = "global", target = "claude") {
3315
3511
  if (target === "all") {
3316
3512
  const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
3317
3513
  const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
3318
- return claudeRemoved || geminiRemoved;
3514
+ const codexRemoved = removeHookForTarget(shortName, scope, "codex");
3515
+ return claudeRemoved || geminiRemoved || codexRemoved;
3319
3516
  }
3320
3517
  if (target === "codewith") {
3321
3518
  return false;
@@ -3333,21 +3530,21 @@ function removeHookForTarget(name, scope, target) {
3333
3530
 
3334
3531
  // src/lib/profiles.ts
3335
3532
  init_app_home();
3336
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
3533
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
3337
3534
  import { join as join8 } from "path";
3338
3535
  function getProfilesDir() {
3339
3536
  const newDir = join8(getEffectiveDataRoot(), "profiles");
3340
3537
  const oldDir = join8(getHomeDir(), ".hooks", "profiles");
3341
- if (!existsSync8(newDir) && existsSync8(oldDir)) {
3342
- mkdirSync5(newDir, { recursive: true });
3538
+ if (!existsSync9(newDir) && existsSync9(oldDir)) {
3539
+ mkdirSync6(newDir, { recursive: true });
3343
3540
  cpSync2(oldDir, newDir, { recursive: true });
3344
3541
  }
3345
3542
  return newDir;
3346
3543
  }
3347
3544
  function ensureProfilesDir() {
3348
3545
  const dir = getProfilesDir();
3349
- if (!existsSync8(dir)) {
3350
- mkdirSync5(dir, { recursive: true });
3546
+ if (!existsSync9(dir)) {
3547
+ mkdirSync6(dir, { recursive: true });
3351
3548
  }
3352
3549
  }
3353
3550
  function profilePath(id) {
@@ -3370,30 +3567,30 @@ function createProfile(input) {
3370
3567
  if (input.name) {
3371
3568
  profile.name = input.name;
3372
3569
  }
3373
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
3570
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
3374
3571
  `);
3375
3572
  return profile;
3376
3573
  }
3377
3574
  function getProfile(id) {
3378
3575
  const path = profilePath(id);
3379
3576
  try {
3380
- if (!existsSync8(path))
3577
+ if (!existsSync9(path))
3381
3578
  return null;
3382
- return JSON.parse(readFileSync5(path, "utf-8"));
3579
+ return JSON.parse(readFileSync6(path, "utf-8"));
3383
3580
  } catch {
3384
3581
  return null;
3385
3582
  }
3386
3583
  }
3387
3584
  function listProfiles() {
3388
3585
  const dir = getProfilesDir();
3389
- if (!existsSync8(dir))
3586
+ if (!existsSync9(dir))
3390
3587
  return [];
3391
3588
  try {
3392
3589
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
3393
3590
  const profiles = [];
3394
3591
  for (const file of files) {
3395
3592
  try {
3396
- const content = readFileSync5(join8(dir, file), "utf-8");
3593
+ const content = readFileSync6(join8(dir, file), "utf-8");
3397
3594
  profiles.push(JSON.parse(content));
3398
3595
  } catch {}
3399
3596
  }
@@ -3925,13 +4122,13 @@ function coerceForSqlite(value) {
3925
4122
  return String(value);
3926
4123
  }
3927
4124
  // src/mcp/server.ts
3928
- var __dirname3 = dirname6(fileURLToPath3(import.meta.url));
4125
+ var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
3929
4126
  var pkg = { name: "@hasna/hooks", version: "0.0.0" };
3930
4127
  try {
3931
4128
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
3932
4129
  const p = join11(__dirname3, rel);
3933
- if (existsSync10(p)) {
3934
- pkg = JSON.parse(readFileSync7(p, "utf-8"));
4130
+ if (existsSync11(p)) {
4131
+ pkg = JSON.parse(readFileSync8(p, "utf-8"));
3935
4132
  break;
3936
4133
  }
3937
4134
  }
@@ -3940,10 +4137,10 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
3940
4137
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
3941
4138
  const resolved = resolveHook2(name);
3942
4139
  const hookScript = resolveScriptPath2(name);
3943
- if (!resolved || !hookScript || !existsSync10(hookScript)) {
4140
+ if (!resolved || !hookScript || !existsSync11(hookScript)) {
3944
4141
  throw new Error(`Hook '${name}' not found`);
3945
4142
  }
3946
- const content = readFileSync7(hookScript);
4143
+ const content = readFileSync8(hookScript);
3947
4144
  const sha = sha256Of(content);
3948
4145
  const check = checkScriptHash(name, sha);
3949
4146
  if (!check.ok) {
@@ -4149,7 +4346,7 @@ function createHooksServer() {
4149
4346
  const settingsPath = getSettingsPath(scope);
4150
4347
  const issues = [];
4151
4348
  const healthy = [];
4152
- const settingsExist = existsSync10(settingsPath);
4349
+ const settingsExist = existsSync11(settingsPath);
4153
4350
  if (!settingsExist) {
4154
4351
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
4155
4352
  }
@@ -4159,7 +4356,7 @@ function createHooksServer() {
4159
4356
  const meta = custom ? resolveHookMeta(name) : getHook(name);
4160
4357
  let hookHealthy = true;
4161
4358
  if (custom) {
4162
- if (!existsSync10(custom.scriptPath)) {
4359
+ if (!existsSync11(custom.scriptPath)) {
4163
4360
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
4164
4361
  hookHealthy = false;
4165
4362
  }
@@ -4169,14 +4366,14 @@ function createHooksServer() {
4169
4366
  continue;
4170
4367
  }
4171
4368
  const hookDir = getHookPath(name);
4172
- if (!existsSync10(join11(hookDir, "src", "hook.ts"))) {
4369
+ if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
4173
4370
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4174
4371
  hookHealthy = false;
4175
4372
  }
4176
4373
  }
4177
4374
  if (meta && settingsExist) {
4178
4375
  try {
4179
- const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4376
+ const settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4180
4377
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
4181
4378
  const eventName = meta.event.split(":")[0];
4182
4379
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -4215,8 +4412,8 @@ function createHooksServer() {
4215
4412
  const hookPath = getHookPath(name);
4216
4413
  const readmePath = join11(hookPath, "README.md");
4217
4414
  let readme = "";
4218
- if (existsSync10(readmePath)) {
4219
- readme = readFileSync7(readmePath, "utf-8");
4415
+ if (existsSync11(readmePath)) {
4416
+ readme = readFileSync8(readmePath, "utf-8");
4220
4417
  }
4221
4418
  if (verbose) {
4222
4419
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -4399,7 +4596,7 @@ function createHooksServer() {
4399
4596
  const ctx = {
4400
4597
  scope,
4401
4598
  settings_path: settingsPath,
4402
- settings_exists: existsSync10(settingsPath),
4599
+ settings_exists: existsSync11(settingsPath),
4403
4600
  registered_hooks: hooks,
4404
4601
  hook_count: hooks.length,
4405
4602
  healthy,
@@ -4545,8 +4742,8 @@ function createHooksServer() {
4545
4742
  const settingsPath = getSettingsPath(scope);
4546
4743
  let settings = {};
4547
4744
  try {
4548
- if (existsSync10(settingsPath))
4549
- settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4745
+ if (existsSync11(settingsPath))
4746
+ settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4550
4747
  } catch {}
4551
4748
  if (!settings.hooks)
4552
4749
  settings.hooks = {};
@@ -4554,10 +4751,10 @@ function createHooksServer() {
4554
4751
  if (!disabled.includes(name))
4555
4752
  disabled.push(name);
4556
4753
  settings.hooks.__disabled = disabled;
4557
- const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync6 } = await import("fs");
4558
- const { dirname: dirname7 } = await import("path");
4559
- mkdirSync6(dirname7(settingsPath), { recursive: true });
4560
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
4754
+ const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync7 } = await import("fs");
4755
+ const { dirname: dirname8 } = await import("path");
4756
+ mkdirSync7(dirname8(settingsPath), { recursive: true });
4757
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
4561
4758
  `);
4562
4759
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
4563
4760
  });
@@ -4568,15 +4765,15 @@ function createHooksServer() {
4568
4765
  const settingsPath = getSettingsPath(scope);
4569
4766
  let settings = {};
4570
4767
  try {
4571
- if (existsSync10(settingsPath))
4572
- settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4768
+ if (existsSync11(settingsPath))
4769
+ settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4573
4770
  } catch {}
4574
4771
  if (settings.hooks?.__disabled) {
4575
4772
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
4576
4773
  if (settings.hooks.__disabled.length === 0)
4577
4774
  delete settings.hooks.__disabled;
4578
- const { writeFileSync: writeFileSync6 } = await import("fs");
4579
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
4775
+ const { writeFileSync: writeFileSync7 } = await import("fs");
4776
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
4580
4777
  `);
4581
4778
  }
4582
4779
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };