@hasna/hooks 0.9.2 → 0.9.4

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.map((command) => command.trim()).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,37 @@ 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;
3439
+ const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
3440
+ if (target === "codex") {
3441
+ const positions = [];
3442
+ for (const [event, groups] of Object.entries(settings.hooks)) {
3443
+ for (const entry of groups) {
3444
+ for (let index = 0;index < entry.hooks.length; index++) {
3445
+ const command = entry.hooks[index]?.command;
3446
+ const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
3447
+ if (match && (match[1] ?? match[2]) === name) {
3448
+ if (registrationName(command) !== name)
3449
+ throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
3450
+ positions.push({ event, entry, index });
3451
+ }
3452
+ }
3453
+ }
3454
+ }
3455
+ if (positions.length) {
3456
+ if (positions.length !== uniqueEventKeys.length || uniqueEventKeys.some((event) => positions.filter((position) => position.event === event).length !== 1) || positions.some((position) => (position.entry.matcher ?? "") !== (meta.matcher ?? ""))) {
3457
+ throw new Error("Codex hook events, matchers or duplicate registrations require explicit reconciliation; no settings were changed.");
3458
+ }
3459
+ for (const { entry, index } of positions) {
3460
+ entry.hooks[index] = { ...entry.hooks[index], type: "command", command: hookCommand };
3461
+ if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0)
3462
+ entry.hooks[index].timeout = meta.timeoutSeconds;
3463
+ }
3464
+ writeSettings(settings, scope, target);
3465
+ return;
3466
+ }
3467
+ }
3359
3468
  removeHookFromAllEvents(settings, name);
3360
- const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
3361
3469
  for (const eventKey of uniqueEventKeys) {
3362
3470
  if (!settings.hooks[eventKey])
3363
3471
  settings.hooks[eventKey] = [];
@@ -3399,10 +3507,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3399
3507
  if (target === "codewith") {
3400
3508
  const config = readCodewithConfig(scope);
3401
3509
  const registered2 = [];
3402
- const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
3510
+ const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
3403
3511
  let match;
3404
3512
  while (match = re.exec(config)) {
3405
- registered2.push(match[1]);
3513
+ const name = registrationName(match[1]);
3514
+ if (name)
3515
+ registered2.push(name);
3406
3516
  }
3407
3517
  return [...new Set(registered2)];
3408
3518
  }
@@ -3413,12 +3523,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3413
3523
  for (const eventKey of Object.keys(settings.hooks)) {
3414
3524
  for (const entry of settings.hooks[eventKey]) {
3415
3525
  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
- }
3526
+ const name = registrationName(hook.command);
3527
+ if (name)
3528
+ registered.push(name);
3422
3529
  }
3423
3530
  }
3424
3531
  }
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.map((command) => command.trim()).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,37 @@ 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;
5793
+ const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
5794
+ if (target === "codex") {
5795
+ const positions = [];
5796
+ for (const [event, groups] of Object.entries(settings.hooks)) {
5797
+ for (const entry of groups) {
5798
+ for (let index = 0;index < entry.hooks.length; index++) {
5799
+ const command = entry.hooks[index]?.command;
5800
+ const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
5801
+ if (match && (match[1] ?? match[2]) === name) {
5802
+ if (registrationName(command) !== name)
5803
+ throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
5804
+ positions.push({ event, entry, index });
5805
+ }
5806
+ }
5807
+ }
5808
+ }
5809
+ if (positions.length) {
5810
+ if (positions.length !== uniqueEventKeys.length || uniqueEventKeys.some((event) => positions.filter((position) => position.event === event).length !== 1) || positions.some((position) => (position.entry.matcher ?? "") !== (meta.matcher ?? ""))) {
5811
+ throw new Error("Codex hook events, matchers or duplicate registrations require explicit reconciliation; no settings were changed.");
5812
+ }
5813
+ for (const { entry, index } of positions) {
5814
+ entry.hooks[index] = { ...entry.hooks[index], type: "command", command: hookCommand };
5815
+ if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0)
5816
+ entry.hooks[index].timeout = meta.timeoutSeconds;
5817
+ }
5818
+ writeSettings(settings, scope, target);
5819
+ return;
5820
+ }
5821
+ }
5712
5822
  removeHookFromAllEvents(settings, name);
5713
- const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
5714
5823
  for (const eventKey of uniqueEventKeys) {
5715
5824
  if (!settings.hooks[eventKey])
5716
5825
  settings.hooks[eventKey] = [];
@@ -5755,10 +5864,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
5755
5864
  if (target === "codewith") {
5756
5865
  const config = readCodewithConfig(scope);
5757
5866
  const registered2 = [];
5758
- const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
5867
+ const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
5759
5868
  let match;
5760
5869
  while (match = re.exec(config)) {
5761
- registered2.push(match[1]);
5870
+ const name = registrationName(match[1]);
5871
+ if (name)
5872
+ registered2.push(name);
5762
5873
  }
5763
5874
  return [...new Set(registered2)];
5764
5875
  }
@@ -5769,12 +5880,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
5769
5880
  for (const eventKey of Object.keys(settings.hooks)) {
5770
5881
  for (const entry of settings.hooks[eventKey]) {
5771
5882
  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
- }
5883
+ const name = registrationName(hook.command);
5884
+ if (name)
5885
+ registered.push(name);
5778
5886
  }
5779
5887
  }
5780
5888
  }
@@ -5876,7 +5984,7 @@ function pruneStaleRegistrations(scope = "global") {
5876
5984
  }
5877
5985
  function removeCodewithHookEntry(configText, name) {
5878
5986
  const sections = configText.split(/\n\s*\n/);
5879
- const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
5987
+ const commandRe = /^command\s*=\s*"([^"\n]+)"$/;
5880
5988
  const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
5881
5989
  const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
5882
5990
  const eventsWithEntries = new Set;
@@ -5889,7 +5997,7 @@ function removeCodewithHookEntry(configText, name) {
5889
5997
  if (entryMatch) {
5890
5998
  const event = entryMatch[1];
5891
5999
  eventsWithEntries.add(event);
5892
- if (lines.some((line) => nameRe.test(line.trim()))) {
6000
+ if (lines.some((line) => registrationName(commandRe.exec(line.trim())?.[1]) === name)) {
5893
6001
  removed = true;
5894
6002
  continue;
5895
6003
  }
@@ -13135,7 +13243,15 @@ program2.command("init").description("Register a new agent profile with a unique
13135
13243
  console.log(` hooks install gitguard --profile ${profile.agent_id}`);
13136
13244
  console.log();
13137
13245
  });
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) => {
13246
+ 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) => {
13247
+ let mementos;
13248
+ try {
13249
+ mementos = mementosRegistrationFromFlags(hook, options);
13250
+ } catch (error) {
13251
+ console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Invalid Mementos options" }));
13252
+ process.exitCode = 1;
13253
+ return;
13254
+ }
13139
13255
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
13140
13256
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
13141
13257
  const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
@@ -13195,7 +13311,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13195
13311
  content,
13196
13312
  args,
13197
13313
  stdin: hookStdin,
13198
- env: process.env,
13314
+ env: mementosRunEnvironment(process.env, mementos),
13199
13315
  timeout
13200
13316
  }));
13201
13317
  } catch (err) {
@@ -13255,7 +13371,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13255
13371
  process.stderr.write(stderr);
13256
13372
  process.exit(exitCode);
13257
13373
  });
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) => {
13374
+ 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
13375
  const scope = resolveScope(options);
13260
13376
  const target = resolveTarget(options);
13261
13377
  let toInstall = hooks;
@@ -13279,6 +13395,18 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
13279
13395
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
13280
13396
  return;
13281
13397
  }
13398
+ let mementos;
13399
+ try {
13400
+ mementos = mementosRegistrationFromFlags(toInstall.length === 1 ? toInstall[0] : "", options);
13401
+ } catch (error) {
13402
+ const message = error instanceof Error ? error.message : "Invalid Mementos options";
13403
+ if (options.json)
13404
+ console.log(JSON.stringify({ error: message, applied: false }));
13405
+ else
13406
+ console.error(message);
13407
+ process.exitCode = 1;
13408
+ return;
13409
+ }
13282
13410
  if (options.applyCodewith && (target === "codewith" || target === "all") && !options.codewithConfig) {
13283
13411
  const message = "--apply-codewith requires --codewith-config <path>; refusing to write default ~/.codewith/config.toml.";
13284
13412
  if (options.json) {
@@ -13317,6 +13445,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
13317
13445
  console.log(JSON.stringify({
13318
13446
  dryRun: true,
13319
13447
  would_install: known,
13448
+ ...mementos ? { mementos } : {},
13320
13449
  custom_sources: customSources,
13321
13450
  pinned: pinnedRequests.map((p) => `${p.pinned.name}@${p.pinned.version}`),
13322
13451
  unknown,
@@ -13421,7 +13550,8 @@ Dry run \u2014 would install (${scope}, ${target}):
13421
13550
  target,
13422
13551
  profile: options.profile,
13423
13552
  codewithMode: options.applyCodewith ? "write" : "fragment",
13424
- codewithConfigPath: options.codewithConfig
13553
+ codewithConfigPath: options.codewithConfig,
13554
+ mementos
13425
13555
  });
13426
13556
  results.push(result);
13427
13557
  }
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.map((command) => command.trim()).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,37 @@ 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;
8018
+ const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
8019
+ if (target === "codex") {
8020
+ const positions = [];
8021
+ for (const [event, groups] of Object.entries(settings.hooks)) {
8022
+ for (const entry of groups) {
8023
+ for (let index = 0;index < entry.hooks.length; index++) {
8024
+ const command = entry.hooks[index]?.command;
8025
+ const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
8026
+ if (match && (match[1] ?? match[2]) === name) {
8027
+ if (registrationName(command) !== name)
8028
+ throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
8029
+ positions.push({ event, entry, index });
8030
+ }
8031
+ }
8032
+ }
8033
+ }
8034
+ if (positions.length) {
8035
+ if (positions.length !== uniqueEventKeys.length || uniqueEventKeys.some((event) => positions.filter((position) => position.event === event).length !== 1) || positions.some((position) => (position.entry.matcher ?? "") !== (meta.matcher ?? ""))) {
8036
+ throw new Error("Codex hook events, matchers or duplicate registrations require explicit reconciliation; no settings were changed.");
8037
+ }
8038
+ for (const { entry, index } of positions) {
8039
+ entry.hooks[index] = { ...entry.hooks[index], type: "command", command: hookCommand };
8040
+ if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0)
8041
+ entry.hooks[index].timeout = meta.timeoutSeconds;
8042
+ }
8043
+ writeSettings(settings, scope, target);
8044
+ return;
8045
+ }
8046
+ }
7940
8047
  removeHookFromAllEvents(settings, name);
7941
- const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
7942
8048
  for (const eventKey of uniqueEventKeys) {
7943
8049
  if (!settings.hooks[eventKey])
7944
8050
  settings.hooks[eventKey] = [];
@@ -7983,10 +8089,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
7983
8089
  if (target === "codewith") {
7984
8090
  const config = readCodewithConfig(scope);
7985
8091
  const registered2 = [];
7986
- const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
8092
+ const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
7987
8093
  let match;
7988
8094
  while (match = re.exec(config)) {
7989
- registered2.push(match[1]);
8095
+ const name = registrationName(match[1]);
8096
+ if (name)
8097
+ registered2.push(name);
7990
8098
  }
7991
8099
  return [...new Set(registered2)];
7992
8100
  }
@@ -7997,12 +8105,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
7997
8105
  for (const eventKey of Object.keys(settings.hooks)) {
7998
8106
  for (const entry of settings.hooks[eventKey]) {
7999
8107
  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
- }
8108
+ const name = registrationName(hook.command);
8109
+ if (name)
8110
+ registered.push(name);
8006
8111
  }
8007
8112
  }
8008
8113
  }
@@ -8036,7 +8141,7 @@ function removeHookForTarget(name, scope, target) {
8036
8141
  var JSON_SETTINGS_TARGETS = ["claude", "gemini", "codex"];
8037
8142
  function removeCodewithHookEntry(configText, name) {
8038
8143
  const sections = configText.split(/\n\s*\n/);
8039
- const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
8144
+ const commandRe = /^command\s*=\s*"([^"\n]+)"$/;
8040
8145
  const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
8041
8146
  const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
8042
8147
  const eventsWithEntries = new Set;
@@ -8049,7 +8154,7 @@ function removeCodewithHookEntry(configText, name) {
8049
8154
  if (entryMatch) {
8050
8155
  const event = entryMatch[1];
8051
8156
  eventsWithEntries.add(event);
8052
- if (lines.some((line) => nameRe.test(line.trim()))) {
8157
+ if (lines.some((line) => registrationName(commandRe.exec(line.trim())?.[1]) === name)) {
8053
8158
  removed = true;
8054
8159
  continue;
8055
8160
  }
@@ -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.4",
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": {