@hasna/hooks 0.9.2 → 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
@@ -2679,6 +2679,79 @@ import { fileURLToPath as fileURLToPath2 } from "url";
2679
2679
 
2680
2680
  // src/lib/registration.ts
2681
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
+ }
2682
2755
  function matchersOverlap(a, b) {
2683
2756
  return a === b || a.includes(b) || b.includes(a);
2684
2757
  }
@@ -3130,12 +3203,13 @@ function codewithStatusMessage(name) {
3130
3203
  return `Running ${name}`;
3131
3204
  }
3132
3205
  }
3133
- function buildCodewithTomlFragment(name, profile) {
3206
+ function buildCodewithTomlFragment(name, profile, mementos) {
3134
3207
  const shortName = shortHookName(name);
3135
3208
  const meta = resolveHookMeta(shortName);
3136
3209
  if (!meta)
3137
3210
  throw new Error(`Hook '${shortName}' not found`);
3138
- 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);
3139
3213
  const matcher = codewithMatcher(meta.matcher);
3140
3214
  const fragments = [];
3141
3215
  for (const event of getHookEvents(meta)) {
@@ -3217,14 +3291,14 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
3217
3291
  }
3218
3292
  return;
3219
3293
  }
3220
- function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
3294
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
3221
3295
  const shortName = shortHookName(name);
3222
3296
  if (!hookExists(shortName)) {
3223
3297
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
3224
3298
  }
3225
3299
  if (target === "codewith") {
3226
3300
  try {
3227
- const fragment = buildCodewithTomlFragment(shortName, profile);
3301
+ const fragment = buildCodewithTomlFragment(shortName, profile, mementos);
3228
3302
  if (codewithMode !== "write") {
3229
3303
  return {
3230
3304
  hook: shortName,
@@ -3281,7 +3355,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
3281
3355
  if (rewriteConflict) {
3282
3356
  return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
3283
3357
  }
3284
- registerHook(shortName, scope, target, profile);
3358
+ registerHook(shortName, scope, target, profile, mementos);
3285
3359
  return {
3286
3360
  hook: shortName,
3287
3361
  success: true,
@@ -3300,7 +3374,12 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
3300
3374
  }
3301
3375
  }
3302
3376
  function installHook(name, options = {}) {
3303
- 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
+ }
3304
3383
  if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
3305
3384
  return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
3306
3385
  }
@@ -3318,7 +3397,7 @@ function installHook(name, options = {}) {
3318
3397
  };
3319
3398
  }
3320
3399
  }
3321
- const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
3400
+ const claudeResult = installForTarget(name, scope, overwrite, "claude", profile, "fragment", undefined, mementos);
3322
3401
  if (!claudeResult.success) {
3323
3402
  return {
3324
3403
  ...claudeResult,
@@ -3326,7 +3405,7 @@ function installHook(name, options = {}) {
3326
3405
  target: "all"
3327
3406
  };
3328
3407
  }
3329
- const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
3408
+ const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath, mementos);
3330
3409
  if (!codewithResult.success) {
3331
3410
  return {
3332
3411
  ...codewithResult,
@@ -3336,9 +3415,9 @@ function installHook(name, options = {}) {
3336
3415
  }
3337
3416
  return { ...claudeResult, target: "all" };
3338
3417
  }
3339
- return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
3418
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
3340
3419
  }
3341
- function registerHook(name, scope = "global", target = "claude", profile) {
3420
+ function registerHook(name, scope = "global", target = "claude", profile, mementos) {
3342
3421
  const meta = resolveHookMeta(name);
3343
3422
  if (!meta)
3344
3423
  return;
@@ -3356,8 +3435,9 @@ function registerHook(name, scope = "global", target = "claude", profile) {
3356
3435
  const settings = readSettings(scope, target);
3357
3436
  if (!settings.hooks)
3358
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;
3359
3439
  removeHookFromAllEvents(settings, name);
3360
- 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);
3361
3441
  for (const eventKey of uniqueEventKeys) {
3362
3442
  if (!settings.hooks[eventKey])
3363
3443
  settings.hooks[eventKey] = [];
@@ -3399,10 +3479,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3399
3479
  if (target === "codewith") {
3400
3480
  const config = readCodewithConfig(scope);
3401
3481
  const registered2 = [];
3402
- const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
3482
+ const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
3403
3483
  let match;
3404
3484
  while (match = re.exec(config)) {
3405
- registered2.push(match[1]);
3485
+ const name = registrationName(match[1]);
3486
+ if (name)
3487
+ registered2.push(name);
3406
3488
  }
3407
3489
  return [...new Set(registered2)];
3408
3490
  }
@@ -3413,12 +3495,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3413
3495
  for (const eventKey of Object.keys(settings.hooks)) {
3414
3496
  for (const entry of settings.hooks[eventKey]) {
3415
3497
  for (const hook of entry.hooks || []) {
3416
- const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
3417
- const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
3418
- const match = newMatch || oldMatch;
3419
- if (match) {
3420
- registered.push(match[1]);
3421
- }
3498
+ const name = registrationName(hook.command);
3499
+ if (name)
3500
+ registered.push(name);
3422
3501
  }
3423
3502
  }
3424
3503
  }
package/bin/index.js CHANGED
@@ -4500,10 +4500,84 @@ var init_resolve = __esm(() => {
4500
4500
  init_manifest();
4501
4501
  });
4502
4502
 
4503
+ // src/lib/mementos-options.ts
4504
+ function validateMementosRegistration(name, value) {
4505
+ if (value === undefined)
4506
+ return;
4507
+ 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") {
4508
+ throw new Error("Mementos options require the mementos-context hook and an explicit enable/disable choice.");
4509
+ }
4510
+ 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))) {
4511
+ throw new Error("OpenRouter requires an enabled Mementos hook and a valid Secrets reference, never a key value.");
4512
+ }
4513
+ }
4514
+ function mementosRegistrationFromFlags(name, flags) {
4515
+ if (!flags.enableMementos && !flags.disableMementos && flags.openrouterSecretRef === undefined)
4516
+ return;
4517
+ if (Boolean(flags.enableMementos) === Boolean(flags.disableMementos)) {
4518
+ throw new Error("Choose exactly one of --enable-mementos or --disable-mementos.");
4519
+ }
4520
+ const value = { enabled: flags.enableMementos === true, ...flags.openrouterSecretRef !== undefined ? { secretRef: flags.openrouterSecretRef } : {} };
4521
+ validateMementosRegistration(name, value);
4522
+ return value;
4523
+ }
4524
+ function mementosCommandSuffix(value) {
4525
+ validateMementosRegistration("mementos-context", value);
4526
+ if (!value)
4527
+ return "";
4528
+ return value.enabled ? ` --enable-mementos${value.secretRef ? ` --openrouter-secret-ref ${value.secretRef}` : ""}` : " --disable-mementos";
4529
+ }
4530
+ function retainedMementosRegistration(commands) {
4531
+ const values = commands.filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
4532
+ const args = command.trim().split(/\s+/).slice(3);
4533
+ const flags = {};
4534
+ const seen = new Set;
4535
+ while (args.length) {
4536
+ const key = args.shift();
4537
+ if (seen.has(key))
4538
+ throw new Error("Ambiguous Mementos registration; specify its enablement explicitly.");
4539
+ seen.add(key);
4540
+ if (key === "--profile" && /^[\w-]+$/.test(args[0] ?? ""))
4541
+ args.shift();
4542
+ else if (key === "--enable-mementos")
4543
+ flags.enableMementos = true;
4544
+ else if (key === "--disable-mementos")
4545
+ flags.disableMementos = true;
4546
+ else if (key === "--openrouter-secret-ref" && args.length)
4547
+ flags.openrouterSecretRef = args.shift();
4548
+ else
4549
+ throw new Error("Unrecognized Mementos registration options; specify its enablement explicitly.");
4550
+ }
4551
+ return mementosRegistrationFromFlags("mementos-context", flags);
4552
+ });
4553
+ if (new Set(values.map((value) => JSON.stringify(value))).size > 1) {
4554
+ throw new Error("Conflicting Mementos registrations; specify its enablement explicitly.");
4555
+ }
4556
+ return values[0];
4557
+ }
4558
+ function mementosRunEnvironment(env, value) {
4559
+ validateMementosRegistration("mementos-context", value);
4560
+ if (!value)
4561
+ return env;
4562
+ return {
4563
+ ...env,
4564
+ HOOKS_MEMENTOS_ENABLED: value.enabled && env.HOOKS_MEMENTOS_ENABLED !== "0" ? "1" : "0",
4565
+ ...value.secretRef ? { HOOKS_MEMENTOS_OPENROUTER_SECRET_REF: value.secretRef } : {}
4566
+ };
4567
+ }
4568
+
4503
4569
  // src/lib/registration.ts
4504
4570
  function registrationName(command) {
4505
4571
  if (typeof command !== "string")
4506
4572
  return;
4573
+ if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
4574
+ try {
4575
+ retainedMementosRegistration([command.trim()]);
4576
+ return "mementos-context";
4577
+ } catch {
4578
+ return;
4579
+ }
4580
+ }
4507
4581
  const match = command.trim().match(REGISTRATION_COMMAND_RE);
4508
4582
  return match ? match[1] ?? match[2] : undefined;
4509
4583
  }
@@ -5483,12 +5557,13 @@ function codewithStatusMessage(name) {
5483
5557
  return `Running ${name}`;
5484
5558
  }
5485
5559
  }
5486
- function buildCodewithTomlFragment(name, profile) {
5560
+ function buildCodewithTomlFragment(name, profile, mementos) {
5487
5561
  const shortName = shortHookName(name);
5488
5562
  const meta = resolveHookMeta(shortName);
5489
5563
  if (!meta)
5490
5564
  throw new Error(`Hook '${shortName}' not found`);
5491
- const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
5565
+ validateMementosRegistration(shortName, mementos);
5566
+ const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
5492
5567
  const matcher = codewithMatcher(meta.matcher);
5493
5568
  const fragments = [];
5494
5569
  for (const event of getHookEvents(meta)) {
@@ -5570,14 +5645,14 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
5570
5645
  }
5571
5646
  return;
5572
5647
  }
5573
- function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
5648
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
5574
5649
  const shortName = shortHookName(name);
5575
5650
  if (!hookExists(shortName)) {
5576
5651
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
5577
5652
  }
5578
5653
  if (target === "codewith") {
5579
5654
  try {
5580
- const fragment = buildCodewithTomlFragment(shortName, profile);
5655
+ const fragment = buildCodewithTomlFragment(shortName, profile, mementos);
5581
5656
  if (codewithMode !== "write") {
5582
5657
  return {
5583
5658
  hook: shortName,
@@ -5634,7 +5709,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
5634
5709
  if (rewriteConflict) {
5635
5710
  return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
5636
5711
  }
5637
- registerHook(shortName, scope, target, profile);
5712
+ registerHook(shortName, scope, target, profile, mementos);
5638
5713
  return {
5639
5714
  hook: shortName,
5640
5715
  success: true,
@@ -5653,7 +5728,12 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
5653
5728
  }
5654
5729
  }
5655
5730
  function installHook(name, options = {}) {
5656
- const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
5731
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
5732
+ try {
5733
+ validateMementosRegistration(name, mementos);
5734
+ } catch (error) {
5735
+ return { hook: name, success: false, target, error: error instanceof Error ? error.message : "Invalid Mementos options" };
5736
+ }
5657
5737
  if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
5658
5738
  return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
5659
5739
  }
@@ -5671,7 +5751,7 @@ function installHook(name, options = {}) {
5671
5751
  };
5672
5752
  }
5673
5753
  }
5674
- const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
5754
+ const claudeResult = installForTarget(name, scope, overwrite, "claude", profile, "fragment", undefined, mementos);
5675
5755
  if (!claudeResult.success) {
5676
5756
  return {
5677
5757
  ...claudeResult,
@@ -5679,7 +5759,7 @@ function installHook(name, options = {}) {
5679
5759
  target: "all"
5680
5760
  };
5681
5761
  }
5682
- const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
5762
+ const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath, mementos);
5683
5763
  if (!codewithResult.success) {
5684
5764
  return {
5685
5765
  ...codewithResult,
@@ -5689,9 +5769,9 @@ function installHook(name, options = {}) {
5689
5769
  }
5690
5770
  return { ...claudeResult, target: "all" };
5691
5771
  }
5692
- return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
5772
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
5693
5773
  }
5694
- function registerHook(name, scope = "global", target = "claude", profile) {
5774
+ function registerHook(name, scope = "global", target = "claude", profile, mementos) {
5695
5775
  const meta = resolveHookMeta(name);
5696
5776
  if (!meta)
5697
5777
  return;
@@ -5709,8 +5789,9 @@ function registerHook(name, scope = "global", target = "claude", profile) {
5709
5789
  const settings = readSettings(scope, target);
5710
5790
  if (!settings.hooks)
5711
5791
  settings.hooks = {};
5792
+ 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;
5712
5793
  removeHookFromAllEvents(settings, name);
5713
- const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
5794
+ const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
5714
5795
  for (const eventKey of uniqueEventKeys) {
5715
5796
  if (!settings.hooks[eventKey])
5716
5797
  settings.hooks[eventKey] = [];
@@ -5755,10 +5836,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
5755
5836
  if (target === "codewith") {
5756
5837
  const config = readCodewithConfig(scope);
5757
5838
  const registered2 = [];
5758
- const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
5839
+ const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
5759
5840
  let match;
5760
5841
  while (match = re.exec(config)) {
5761
- registered2.push(match[1]);
5842
+ const name = registrationName(match[1]);
5843
+ if (name)
5844
+ registered2.push(name);
5762
5845
  }
5763
5846
  return [...new Set(registered2)];
5764
5847
  }
@@ -5769,12 +5852,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
5769
5852
  for (const eventKey of Object.keys(settings.hooks)) {
5770
5853
  for (const entry of settings.hooks[eventKey]) {
5771
5854
  for (const hook of entry.hooks || []) {
5772
- const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
5773
- const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
5774
- const match = newMatch || oldMatch;
5775
- if (match) {
5776
- registered.push(match[1]);
5777
- }
5855
+ const name = registrationName(hook.command);
5856
+ if (name)
5857
+ registered.push(name);
5778
5858
  }
5779
5859
  }
5780
5860
  }
@@ -5876,7 +5956,7 @@ function pruneStaleRegistrations(scope = "global") {
5876
5956
  }
5877
5957
  function removeCodewithHookEntry(configText, name) {
5878
5958
  const sections = configText.split(/\n\s*\n/);
5879
- const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
5959
+ const commandRe = /^command\s*=\s*"([^"\n]+)"$/;
5880
5960
  const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
5881
5961
  const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
5882
5962
  const eventsWithEntries = new Set;
@@ -5889,7 +5969,7 @@ function removeCodewithHookEntry(configText, name) {
5889
5969
  if (entryMatch) {
5890
5970
  const event = entryMatch[1];
5891
5971
  eventsWithEntries.add(event);
5892
- if (lines.some((line) => nameRe.test(line.trim()))) {
5972
+ if (lines.some((line) => registrationName(commandRe.exec(line.trim())?.[1]) === name)) {
5893
5973
  removed = true;
5894
5974
  continue;
5895
5975
  }
@@ -13135,7 +13215,15 @@ program2.command("init").description("Register a new agent profile with a unique
13135
13215
  console.log(` hooks install gitguard --profile ${profile.agent_id}`);
13136
13216
  console.log();
13137
13217
  });
13138
- program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
13218
+ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").option("--enable-mementos", "Enable mementos-context for this invocation (provider assistance remains separately opt-in)").option("--disable-mementos", "Disable mementos-context for this invocation").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
13219
+ let mementos;
13220
+ try {
13221
+ mementos = mementosRegistrationFromFlags(hook, options);
13222
+ } catch (error) {
13223
+ console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Invalid Mementos options" }));
13224
+ process.exitCode = 1;
13225
+ return;
13226
+ }
13139
13227
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
13140
13228
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
13141
13229
  const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
@@ -13195,7 +13283,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13195
13283
  content,
13196
13284
  args,
13197
13285
  stdin: hookStdin,
13198
- env: process.env,
13286
+ env: mementosRunEnvironment(process.env, mementos),
13199
13287
  timeout
13200
13288
  }));
13201
13289
  } catch (err) {
@@ -13255,7 +13343,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13255
13343
  process.stderr.write(stderr);
13256
13344
  process.exit(exitCode);
13257
13345
  });
13258
- program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
13346
+ program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--enable-mementos", "Persistently enable mementos-context (provider assistance remains separately opt-in)").option("--disable-mementos", "Persistently disable mementos-context").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
13259
13347
  const scope = resolveScope(options);
13260
13348
  const target = resolveTarget(options);
13261
13349
  let toInstall = hooks;
@@ -13279,6 +13367,18 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
13279
13367
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
13280
13368
  return;
13281
13369
  }
13370
+ let mementos;
13371
+ try {
13372
+ mementos = mementosRegistrationFromFlags(toInstall.length === 1 ? toInstall[0] : "", options);
13373
+ } catch (error) {
13374
+ const message = error instanceof Error ? error.message : "Invalid Mementos options";
13375
+ if (options.json)
13376
+ console.log(JSON.stringify({ error: message, applied: false }));
13377
+ else
13378
+ console.error(message);
13379
+ process.exitCode = 1;
13380
+ return;
13381
+ }
13282
13382
  if (options.applyCodewith && (target === "codewith" || target === "all") && !options.codewithConfig) {
13283
13383
  const message = "--apply-codewith requires --codewith-config <path>; refusing to write default ~/.codewith/config.toml.";
13284
13384
  if (options.json) {
@@ -13317,6 +13417,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
13317
13417
  console.log(JSON.stringify({
13318
13418
  dryRun: true,
13319
13419
  would_install: known,
13420
+ ...mementos ? { mementos } : {},
13320
13421
  custom_sources: customSources,
13321
13422
  pinned: pinnedRequests.map((p) => `${p.pinned.name}@${p.pinned.version}`),
13322
13423
  unknown,
@@ -13421,7 +13522,8 @@ Dry run \u2014 would install (${scope}, ${target}):
13421
13522
  target,
13422
13523
  profile: options.profile,
13423
13524
  codewithMode: options.applyCodewith ? "write" : "fragment",
13424
- codewithConfigPath: options.codewithConfig
13525
+ codewithConfigPath: options.codewithConfig,
13526
+ mementos
13425
13527
  });
13426
13528
  results.push(result);
13427
13529
  }
package/dist/index.js CHANGED
@@ -7445,7 +7445,78 @@ function resolveHook(name) {
7445
7445
  // src/lib/installer.ts
7446
7446
  init_manifest();
7447
7447
 
7448
+ // src/lib/mementos-options.ts
7449
+ function validateMementosRegistration(name, value) {
7450
+ if (value === undefined)
7451
+ return;
7452
+ 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") {
7453
+ throw new Error("Mementos options require the mementos-context hook and an explicit enable/disable choice.");
7454
+ }
7455
+ 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))) {
7456
+ throw new Error("OpenRouter requires an enabled Mementos hook and a valid Secrets reference, never a key value.");
7457
+ }
7458
+ }
7459
+ function mementosRegistrationFromFlags(name, flags) {
7460
+ if (!flags.enableMementos && !flags.disableMementos && flags.openrouterSecretRef === undefined)
7461
+ return;
7462
+ if (Boolean(flags.enableMementos) === Boolean(flags.disableMementos)) {
7463
+ throw new Error("Choose exactly one of --enable-mementos or --disable-mementos.");
7464
+ }
7465
+ const value = { enabled: flags.enableMementos === true, ...flags.openrouterSecretRef !== undefined ? { secretRef: flags.openrouterSecretRef } : {} };
7466
+ validateMementosRegistration(name, value);
7467
+ return value;
7468
+ }
7469
+ function mementosCommandSuffix(value) {
7470
+ validateMementosRegistration("mementos-context", value);
7471
+ if (!value)
7472
+ return "";
7473
+ return value.enabled ? ` --enable-mementos${value.secretRef ? ` --openrouter-secret-ref ${value.secretRef}` : ""}` : " --disable-mementos";
7474
+ }
7475
+ function retainedMementosRegistration(commands) {
7476
+ const values = commands.filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
7477
+ const args = command.trim().split(/\s+/).slice(3);
7478
+ const flags = {};
7479
+ const seen = new Set;
7480
+ while (args.length) {
7481
+ const key = args.shift();
7482
+ if (seen.has(key))
7483
+ throw new Error("Ambiguous Mementos registration; specify its enablement explicitly.");
7484
+ seen.add(key);
7485
+ if (key === "--profile" && /^[\w-]+$/.test(args[0] ?? ""))
7486
+ args.shift();
7487
+ else if (key === "--enable-mementos")
7488
+ flags.enableMementos = true;
7489
+ else if (key === "--disable-mementos")
7490
+ flags.disableMementos = true;
7491
+ else if (key === "--openrouter-secret-ref" && args.length)
7492
+ flags.openrouterSecretRef = args.shift();
7493
+ else
7494
+ throw new Error("Unrecognized Mementos registration options; specify its enablement explicitly.");
7495
+ }
7496
+ return mementosRegistrationFromFlags("mementos-context", flags);
7497
+ });
7498
+ if (new Set(values.map((value) => JSON.stringify(value))).size > 1) {
7499
+ throw new Error("Conflicting Mementos registrations; specify its enablement explicitly.");
7500
+ }
7501
+ return values[0];
7502
+ }
7503
+
7448
7504
  // src/lib/registration.ts
7505
+ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|hook-([\w-]+))$/;
7506
+ function registrationName(command) {
7507
+ if (typeof command !== "string")
7508
+ return;
7509
+ if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
7510
+ try {
7511
+ retainedMementosRegistration([command.trim()]);
7512
+ return "mementos-context";
7513
+ } catch {
7514
+ return;
7515
+ }
7516
+ }
7517
+ const match = command.trim().match(REGISTRATION_COMMAND_RE);
7518
+ return match ? match[1] ?? match[2] : undefined;
7519
+ }
7449
7520
  function matchersOverlap(a, b) {
7450
7521
  return a === b || a.includes(b) || b.includes(a);
7451
7522
  }
@@ -7711,12 +7782,13 @@ function codewithStatusMessage(name) {
7711
7782
  return `Running ${name}`;
7712
7783
  }
7713
7784
  }
7714
- function buildCodewithTomlFragment(name, profile) {
7785
+ function buildCodewithTomlFragment(name, profile, mementos) {
7715
7786
  const shortName = shortHookName(name);
7716
7787
  const meta = resolveHookMeta(shortName);
7717
7788
  if (!meta)
7718
7789
  throw new Error(`Hook '${shortName}' not found`);
7719
- const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
7790
+ validateMementosRegistration(shortName, mementos);
7791
+ const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
7720
7792
  const matcher = codewithMatcher(meta.matcher);
7721
7793
  const fragments = [];
7722
7794
  for (const event of getHookEvents(meta)) {
@@ -7798,14 +7870,14 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
7798
7870
  }
7799
7871
  return;
7800
7872
  }
7801
- function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
7873
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
7802
7874
  const shortName = shortHookName(name);
7803
7875
  if (!hookExists(shortName)) {
7804
7876
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
7805
7877
  }
7806
7878
  if (target === "codewith") {
7807
7879
  try {
7808
- const fragment = buildCodewithTomlFragment(shortName, profile);
7880
+ const fragment = buildCodewithTomlFragment(shortName, profile, mementos);
7809
7881
  if (codewithMode !== "write") {
7810
7882
  return {
7811
7883
  hook: shortName,
@@ -7862,7 +7934,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
7862
7934
  if (rewriteConflict) {
7863
7935
  return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
7864
7936
  }
7865
- registerHook(shortName, scope, target, profile);
7937
+ registerHook(shortName, scope, target, profile, mementos);
7866
7938
  return {
7867
7939
  hook: shortName,
7868
7940
  success: true,
@@ -7881,7 +7953,12 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
7881
7953
  }
7882
7954
  }
7883
7955
  function installHook(name, options = {}) {
7884
- const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
7956
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
7957
+ try {
7958
+ validateMementosRegistration(name, mementos);
7959
+ } catch (error) {
7960
+ return { hook: name, success: false, target, error: error instanceof Error ? error.message : "Invalid Mementos options" };
7961
+ }
7885
7962
  if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
7886
7963
  return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
7887
7964
  }
@@ -7899,7 +7976,7 @@ function installHook(name, options = {}) {
7899
7976
  };
7900
7977
  }
7901
7978
  }
7902
- const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
7979
+ const claudeResult = installForTarget(name, scope, overwrite, "claude", profile, "fragment", undefined, mementos);
7903
7980
  if (!claudeResult.success) {
7904
7981
  return {
7905
7982
  ...claudeResult,
@@ -7907,7 +7984,7 @@ function installHook(name, options = {}) {
7907
7984
  target: "all"
7908
7985
  };
7909
7986
  }
7910
- const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
7987
+ const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath, mementos);
7911
7988
  if (!codewithResult.success) {
7912
7989
  return {
7913
7990
  ...codewithResult,
@@ -7917,9 +7994,9 @@ function installHook(name, options = {}) {
7917
7994
  }
7918
7995
  return { ...claudeResult, target: "all" };
7919
7996
  }
7920
- return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
7997
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
7921
7998
  }
7922
- function registerHook(name, scope = "global", target = "claude", profile) {
7999
+ function registerHook(name, scope = "global", target = "claude", profile, mementos) {
7923
8000
  const meta = resolveHookMeta(name);
7924
8001
  if (!meta)
7925
8002
  return;
@@ -7937,8 +8014,9 @@ function registerHook(name, scope = "global", target = "claude", profile) {
7937
8014
  const settings = readSettings(scope, target);
7938
8015
  if (!settings.hooks)
7939
8016
  settings.hooks = {};
8017
+ 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;
7940
8018
  removeHookFromAllEvents(settings, name);
7941
- const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
8019
+ const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
7942
8020
  for (const eventKey of uniqueEventKeys) {
7943
8021
  if (!settings.hooks[eventKey])
7944
8022
  settings.hooks[eventKey] = [];
@@ -7983,10 +8061,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
7983
8061
  if (target === "codewith") {
7984
8062
  const config = readCodewithConfig(scope);
7985
8063
  const registered2 = [];
7986
- const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
8064
+ const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
7987
8065
  let match;
7988
8066
  while (match = re.exec(config)) {
7989
- registered2.push(match[1]);
8067
+ const name = registrationName(match[1]);
8068
+ if (name)
8069
+ registered2.push(name);
7990
8070
  }
7991
8071
  return [...new Set(registered2)];
7992
8072
  }
@@ -7997,12 +8077,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
7997
8077
  for (const eventKey of Object.keys(settings.hooks)) {
7998
8078
  for (const entry of settings.hooks[eventKey]) {
7999
8079
  for (const hook of entry.hooks || []) {
8000
- const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
8001
- const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
8002
- const match = newMatch || oldMatch;
8003
- if (match) {
8004
- registered.push(match[1]);
8005
- }
8080
+ const name = registrationName(hook.command);
8081
+ if (name)
8082
+ registered.push(name);
8006
8083
  }
8007
8084
  }
8008
8085
  }
@@ -8036,7 +8113,7 @@ function removeHookForTarget(name, scope, target) {
8036
8113
  var JSON_SETTINGS_TARGETS = ["claude", "gemini", "codex"];
8037
8114
  function removeCodewithHookEntry(configText, name) {
8038
8115
  const sections = configText.split(/\n\s*\n/);
8039
- const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
8116
+ const commandRe = /^command\s*=\s*"([^"\n]+)"$/;
8040
8117
  const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
8041
8118
  const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
8042
8119
  const eventsWithEntries = new Set;
@@ -8049,7 +8126,7 @@ function removeCodewithHookEntry(configText, name) {
8049
8126
  if (entryMatch) {
8050
8127
  const event = entryMatch[1];
8051
8128
  eventsWithEntries.add(event);
8052
- if (lines.some((line) => nameRe.test(line.trim()))) {
8129
+ if (lines.some((line) => registrationName(commandRe.exec(line.trim())?.[1]) === name)) {
8053
8130
  removed = true;
8054
8131
  continue;
8055
8132
  }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { type HookEvent, type HookMeta } from "./registry.js";
13
13
  import { type StaleRegistration } from "./registration.js";
14
+ import { type MementosRegistration } from "./mementos-options.js";
14
15
  export type Scope = "global" | "project";
15
16
  export type Target = "claude" | "gemini" | "codewith" | "codex" | "all";
16
17
  type SingleTarget = Exclude<Target, "all">;
@@ -29,6 +30,8 @@ export interface InstallResult {
29
30
  configPath?: string;
30
31
  }
31
32
  export interface InstallOptions {
33
+ /** Explicit persistent opt-in for mementos-context; omitted updates retain its current choice. */
34
+ mementos?: MementosRegistration;
32
35
  scope?: Scope;
33
36
  overwrite?: boolean;
34
37
  target?: Target;
@@ -47,7 +50,7 @@ export declare function getHookPath(name: string): string;
47
50
  export declare function hookExists(name: string): boolean;
48
51
  /** Whether a hook's event can be registered for the given target */
49
52
  export declare function isEventSupported(internalEvent: HookEvent, target: SingleTarget): boolean;
50
- export declare function buildCodewithTomlFragment(name: string, profile?: string): string;
53
+ export declare function buildCodewithTomlFragment(name: string, profile?: string, mementos?: MementosRegistration): string;
51
54
  /**
52
55
  * The one conflict class that BLOCKS an install: two PreToolUse hooks on
53
56
  * overlapping matchers that both rewrite the tool input
@@ -0,0 +1,16 @@
1
+ /** Persistent opt-in lives in the native hook command, never in a credential file. */
2
+ export interface MementosRegistration {
3
+ enabled: boolean;
4
+ secretRef?: string;
5
+ }
6
+ export interface MementosFlags {
7
+ enableMementos?: boolean;
8
+ disableMementos?: boolean;
9
+ openrouterSecretRef?: string;
10
+ }
11
+ export declare function validateMementosRegistration(name: string, value?: MementosRegistration): void;
12
+ export declare function mementosRegistrationFromFlags(name: string, flags: MementosFlags): MementosRegistration | undefined;
13
+ export declare function mementosCommandSuffix(value?: MementosRegistration): string;
14
+ /** Preserve explicit activation across ordinary install --overwrite / update. */
15
+ export declare function retainedMementosRegistration(commands: string[]): MementosRegistration | undefined;
16
+ export declare function mementosRunEnvironment(env: NodeJS.ProcessEnv, value?: MementosRegistration): NodeJS.ProcessEnv;
@@ -6,6 +6,7 @@
6
6
  * an array of { matcher?, hooks: [...] } entries — so a composite event such
7
7
  * as 'PreToolUse:Bash' must be split before lookup, never used as a key.
8
8
  */
9
+ export declare function registrationName(command: unknown): string | undefined;
9
10
  /** A settings registration whose hook name cannot resolve. */
10
11
  export interface StaleRegistration {
11
12
  /** Settings file the registration lives in. */
@@ -22,8 +22,22 @@ retains the existing Claude/Codewith install group; select Codex explicitly.
22
22
  Use `hooks list --registered --target codex` and
23
23
  `hooks remove mementos-context --target codex` to inspect or remove registration.
24
24
 
25
- The hook does nothing until its calling process sets `HOOKS_MEMENTOS_ENABLED=1`.
26
- For example, start a fresh Codex CLI from a shell with:
25
+ The hook is off by default. To save a one-time opt-in in the native registration:
26
+
27
+ ```sh
28
+ hooks install mementos-context --target codex --overwrite --enable-mementos
29
+ # Review the changed definition in Codex /hooks before trusting it.
30
+ ```
31
+
32
+ New sessions now run the hook without shell exports. Its project is the session's
33
+ working directory, which must resolve to an existing Mementos project. Routine
34
+ JSON-target registration updates retain this choice. To save a persistent opt-out:
35
+
36
+ ```sh
37
+ hooks install mementos-context --target codex --overwrite --disable-mementos
38
+ ```
39
+
40
+ For a temporary opt-in instead, start a fresh Codex CLI from a shell with:
27
41
 
28
42
  ```sh
29
43
  export HOOKS_MEMENTOS_ENABLED=1
@@ -45,6 +59,18 @@ mementos decisions enable --retrieval
45
59
  export HOOKS_MEMENTOS_OPENROUTER_SECRET_REF=example/openrouter/key
46
60
  ```
47
61
 
62
+ To persist the reference with activation, replace that export with:
63
+
64
+ ```sh
65
+ hooks install mementos-context --target codex --overwrite --enable-mementos \
66
+ --openrouter-secret-ref example/openrouter/key
67
+ ```
68
+
69
+ The native command stores only the reference. Existing hook definitions are
70
+ preserved, and native trust is still required after a command changes. Set
71
+ `HOOKS_MEMENTOS_ENABLED=0` to opt out for one session even when its registration
72
+ is enabled. A persistent `--disable-mementos` always disables execution.
73
+
48
74
  The reference must name an existing key available through the optional Secrets
49
75
  CLI. The hook invokes `secrets exec <reference> --as OPENROUTER_API_KEY -- mementos
50
76
  prompt-context ...`; the key reaches only the Mementos consumer. Do not place key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {