@wipcomputer/wip-ldm-os 0.4.75-alpha.1 → 0.4.76

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.
Files changed (3) hide show
  1. package/SKILL.md +1 -1
  2. package/lib/deploy.mjs +51 -17
  3. package/package.json +1 -1
package/SKILL.md CHANGED
@@ -9,7 +9,7 @@ license: MIT
9
9
  compatibility: Requires git, npm, node. Node.js 18+.
10
10
  metadata:
11
11
  display-name: "LDM OS"
12
- version: "0.4.74"
12
+ version: "0.4.76"
13
13
  homepage: "https://github.com/wipcomputer/wip-ldm-os"
14
14
  author: "Parker Todd Brooks"
15
15
  category: infrastructure
package/lib/deploy.mjs CHANGED
@@ -940,7 +940,16 @@ function installClaudeCodeHookEvent(repoPath, door) {
940
940
  // Deploy guard.mjs to ~/.ldm/extensions/{toolName}/ (#85: always update, not just when missing)
941
941
  // Idempotent across multi-door invocations: two doors on the same repo
942
942
  // will both trigger this copy, which is a filesystem no-op after the first.
943
+ //
944
+ // Also recursively copy sibling source subdirectories (e.g. lib/, dist/).
945
+ // Historical behavior only copied guard.mjs + package.json at the root,
946
+ // so a guard.mjs whose imports referenced ./lib/*.mjs loaded fine from
947
+ // source but broke post-install with ERR_MODULE_NOT_FOUND. This caused
948
+ // the wip-branch-guard 1.9.77 incident on 2026-04-20.
943
949
  const srcGuard = join(repoPath, 'guard.mjs');
950
+ const SKIP_DIRS_FOR_HOOK = new Set([
951
+ '.git', 'node_modules', 'ai', '_trash', '.worktrees', 'logs', 'test', 'tests', '__tests__',
952
+ ]);
944
953
  if (existsSync(srcGuard)) {
945
954
  try {
946
955
  if (!existsSync(extDir)) mkdirSync(extDir, { recursive: true });
@@ -948,6 +957,14 @@ function installClaudeCodeHookEvent(repoPath, door) {
948
957
  // Also copy package.json for metadata
949
958
  const srcPkg = join(repoPath, 'package.json');
950
959
  if (existsSync(srcPkg)) copyFileSync(srcPkg, join(extDir, 'package.json'));
960
+ // Recurse sibling subdirs so nested imports (e.g. ./lib/foo.mjs) load.
961
+ for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
962
+ if (!entry.isDirectory()) continue;
963
+ if (SKIP_DIRS_FOR_HOOK.has(entry.name)) continue;
964
+ const srcDir = join(repoPath, entry.name);
965
+ const destDir = join(extDir, entry.name);
966
+ try { cpSync(srcDir, destDir, { recursive: true }); } catch {}
967
+ }
951
968
  } catch (e) {
952
969
  // Non-fatal: fall back to source path
953
970
  }
@@ -967,32 +984,49 @@ function installClaudeCodeHookEvent(repoPath, door) {
967
984
  if (!settings.hooks) settings.hooks = {};
968
985
  if (!settings.hooks[event]) settings.hooks[event] = [];
969
986
 
970
- // Match existing entries by the guard command path + the matcher, so that
971
- // a single extension registering on multiple events (each with its own
972
- // matcher) creates one entry per event rather than all entries colliding
973
- // on the same hook slot. Before this change the existing-entry check was
974
- // per-extension, not per-extension-per-event.
987
+ // Match existing entries by command path alone (same extension + same
988
+ // event). The previous finder required matcher equality too, so when
989
+ // an extension bumped its matcher (e.g. wip-branch-guard 1.9.78 -> 1.9.79
990
+ // added Read|Glob to enable onboarding), the finder missed the old entry
991
+ // and appended a new one, leaving an orphaned old matcher in settings
992
+ // and doubling hook invocations on overlapping matchers.
993
+ //
994
+ // Now: find by extension dir in the command. Update matcher + command +
995
+ // timeout in place. First pass removes any DUPLICATE entries for the same
996
+ // extension in this event slot (orphan cleanup; catches post-1.9.78
997
+ // duplicates on users who already installed the broken version).
975
998
  const doorMatcher = door.matcher || undefined;
976
- const existingIdx = settings.hooks[event].findIndex(entry => {
977
- const sameMatcher = (entry.matcher || undefined) === doorMatcher;
978
- if (!sameMatcher) return false;
979
- return entry.hooks?.some(h => {
980
- const cmd = h.command || '';
981
- return cmd.includes(`/${toolName}/`) || cmd === hookCommand;
982
- });
999
+ const toolTag = `/${toolName}/`;
1000
+ const ownedIdxs = [];
1001
+ settings.hooks[event].forEach((entry, i) => {
1002
+ const hooks = entry.hooks || [];
1003
+ if (hooks.some(h => (h.command || '').includes(toolTag))) ownedIdxs.push(i);
983
1004
  });
1005
+ let removed = 0;
1006
+ if (ownedIdxs.length > 1) {
1007
+ // Keep the first, remove the rest. Walk right-to-left so earlier indices stay valid.
1008
+ for (let j = ownedIdxs.length - 1; j >= 1; j--) {
1009
+ settings.hooks[event].splice(ownedIdxs[j], 1);
1010
+ removed++;
1011
+ }
1012
+ }
1013
+ const existingIdx = ownedIdxs.length > 0 ? ownedIdxs[0] : -1;
984
1014
 
985
1015
  if (existingIdx !== -1) {
986
- const existingCmd = settings.hooks[event][existingIdx].hooks?.[0]?.command || '';
987
- if (existingCmd === hookCommand) {
1016
+ const existingEntry = settings.hooks[event][existingIdx];
1017
+ const existingCmd = existingEntry.hooks?.[0]?.command || '';
1018
+ const existingMatcher = existingEntry.matcher || undefined;
1019
+ if (existingCmd === hookCommand && existingMatcher === doorMatcher && removed === 0) {
988
1020
  skip(`Claude Code: ${event} hook already configured`);
989
1021
  return true;
990
1022
  }
991
- settings.hooks[event][existingIdx].hooks[0].command = hookCommand;
992
- settings.hooks[event][existingIdx].hooks[0].timeout = door.timeout || 10;
1023
+ existingEntry.matcher = doorMatcher;
1024
+ existingEntry.hooks[0].command = hookCommand;
1025
+ existingEntry.hooks[0].timeout = door.timeout || 10;
993
1026
  try {
994
1027
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
995
- ok(`Claude Code: ${event} hook updated`);
1028
+ const note = removed > 0 ? ` (removed ${removed} orphan entr${removed === 1 ? 'y' : 'ies'})` : '';
1029
+ ok(`Claude Code: ${event} hook updated${note}`);
996
1030
  return true;
997
1031
  } catch (e) {
998
1032
  fail(`Claude Code: failed to update settings.json. ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.75-alpha.1",
3
+ "version": "0.4.76",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {