@wipcomputer/wip-ldm-os 0.4.73-alpha.17 → 0.4.73-alpha.18

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/lib/deploy.mjs CHANGED
@@ -812,7 +812,28 @@ function registerMCP(repoPath, door, toolName) {
812
812
  }
813
813
  }
814
814
 
815
- function installClaudeCodeHook(repoPath, door) {
815
+ /**
816
+ * Install Claude Code hook(s) for an extension.
817
+ *
818
+ * Accepts either a single door object (legacy) or an array of door objects
819
+ * (new in 2026-04-05 for wip-branch-guard 1.9.73 which registers on both
820
+ * PreToolUse and SessionStart). Normalizes to an array and installs each
821
+ * door independently.
822
+ *
823
+ * Returns true if at least one door installed successfully.
824
+ */
825
+ function installClaudeCodeHook(repoPath, doorOrDoors) {
826
+ const doors = Array.isArray(doorOrDoors) ? doorOrDoors : [doorOrDoors];
827
+ let anyOk = false;
828
+ for (const door of doors) {
829
+ if (installClaudeCodeHookEvent(repoPath, door)) {
830
+ anyOk = true;
831
+ }
832
+ }
833
+ return anyOk;
834
+ }
835
+
836
+ function installClaudeCodeHookEvent(repoPath, door) {
816
837
  const settingsPath = join(HOME, '.claude', 'settings.json');
817
838
  let settings = readJSON(settingsPath);
818
839
 
@@ -826,6 +847,8 @@ function installClaudeCodeHook(repoPath, door) {
826
847
  const installedGuard = join(extDir, 'guard.mjs');
827
848
 
828
849
  // Deploy guard.mjs to ~/.ldm/extensions/{toolName}/ (#85: always update, not just when missing)
850
+ // Idempotent across multi-door invocations: two doors on the same repo
851
+ // will both trigger this copy, which is a filesystem no-op after the first.
829
852
  const srcGuard = join(repoPath, 'guard.mjs');
830
853
  if (existsSync(srcGuard)) {
831
854
  try {
@@ -843,21 +866,30 @@ function installClaudeCodeHook(repoPath, door) {
843
866
  ? `node ${installedGuard}`
844
867
  : (door.command || `node "${srcGuard}"`);
845
868
 
869
+ const event = door.event || 'PreToolUse';
870
+
846
871
  if (DRY_RUN) {
847
- ok(`Claude Code: would add ${door.event || 'PreToolUse'} hook (dry run)`);
872
+ ok(`Claude Code: would add ${event} hook (dry run)`);
848
873
  return true;
849
874
  }
850
875
 
851
876
  if (!settings.hooks) settings.hooks = {};
852
- const event = door.event || 'PreToolUse';
853
877
  if (!settings.hooks[event]) settings.hooks[event] = [];
854
878
 
855
- const existingIdx = settings.hooks[event].findIndex(entry =>
856
- entry.hooks?.some(h => {
879
+ // Match existing entries by the guard command path + the matcher, so that
880
+ // a single extension registering on multiple events (each with its own
881
+ // matcher) creates one entry per event rather than all entries colliding
882
+ // on the same hook slot. Before this change the existing-entry check was
883
+ // per-extension, not per-extension-per-event.
884
+ const doorMatcher = door.matcher || undefined;
885
+ const existingIdx = settings.hooks[event].findIndex(entry => {
886
+ const sameMatcher = (entry.matcher || undefined) === doorMatcher;
887
+ if (!sameMatcher) return false;
888
+ return entry.hooks?.some(h => {
857
889
  const cmd = h.command || '';
858
890
  return cmd.includes(`/${toolName}/`) || cmd === hookCommand;
859
- })
860
- );
891
+ });
892
+ });
861
893
 
862
894
  if (existingIdx !== -1) {
863
895
  const existingCmd = settings.hooks[event][existingIdx].hooks?.[0]?.command || '';
@@ -878,7 +910,7 @@ function installClaudeCodeHook(repoPath, door) {
878
910
  }
879
911
 
880
912
  settings.hooks[event].push({
881
- matcher: door.matcher || undefined,
913
+ matcher: doorMatcher,
882
914
  hooks: [{
883
915
  type: 'command',
884
916
  command: hookCommand,
package/lib/detect.mjs CHANGED
@@ -53,16 +53,27 @@ export function detectInterfaces(repoPath) {
53
53
  interfaces.skill = { path: join(repoPath, 'SKILL.md') };
54
54
  }
55
55
 
56
- // 6. Claude Code Hook: guard.mjs or claudeCode.hook in package.json
57
- if (pkg?.claudeCode?.hook) {
58
- interfaces.claudeCodeHook = pkg.claudeCode.hook;
56
+ // 6. Claude Code Hook: guard.mjs or claudeCode.hook(s) in package.json
57
+ //
58
+ // Supports three shapes:
59
+ // - Legacy singular: pkg.claudeCode.hook = { event, matcher, ... }
60
+ // - New plural array: pkg.claudeCode.hooks = [{ event, matcher, ... }, ...]
61
+ // (one extension can register on multiple events, e.g. both PreToolUse
62
+ // and SessionStart. Added 2026-04-05 for wip-branch-guard 1.9.73.)
63
+ // - Implicit: a bare guard.mjs file with no package.json declaration.
64
+ //
65
+ // Normalized to an array internally so deploy.mjs has one code path.
66
+ if (Array.isArray(pkg?.claudeCode?.hooks)) {
67
+ interfaces.claudeCodeHook = pkg.claudeCode.hooks;
68
+ } else if (pkg?.claudeCode?.hook) {
69
+ interfaces.claudeCodeHook = [pkg.claudeCode.hook];
59
70
  } else if (existsSync(join(repoPath, 'guard.mjs'))) {
60
- interfaces.claudeCodeHook = {
71
+ interfaces.claudeCodeHook = [{
61
72
  event: 'PreToolUse',
62
73
  matcher: 'Edit|Write',
63
74
  command: `node "${join(repoPath, 'guard.mjs')}"`,
64
75
  timeout: 5,
65
- };
76
+ }];
66
77
  }
67
78
 
68
79
  // 7. Claude Code Plugin: .claude-plugin/plugin.json
@@ -93,7 +104,10 @@ export function describeInterfaces(interfaces) {
93
104
  if (interfaces.mcp) lines.push(`MCP Server: ${interfaces.mcp.file}`);
94
105
  if (interfaces.openclaw) lines.push(`OpenClaw Plugin: ${interfaces.openclaw.config?.name || 'detected'}`);
95
106
  if (interfaces.skill) lines.push(`Skill: SKILL.md`);
96
- if (interfaces.claudeCodeHook) lines.push(`Claude Code Hook: ${interfaces.claudeCodeHook.event || 'PreToolUse'}`);
107
+ if (interfaces.claudeCodeHook) {
108
+ const events = interfaces.claudeCodeHook.map(h => h.event || 'PreToolUse');
109
+ lines.push(`Claude Code Hook: ${events.join(', ')}`);
110
+ }
97
111
  if (interfaces.claudeCodePlugin) lines.push(`Claude Code Plugin: ${interfaces.claudeCodePlugin.manifest?.name || 'detected'}`);
98
112
 
99
113
  return `${names.length} interface(s): ${names.join(', ')}\n${lines.map(l => ` ${l}`).join('\n')}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.73-alpha.17",
3
+ "version": "0.4.73-alpha.18",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {