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

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/ldm.js CHANGED
@@ -544,20 +544,14 @@ function deployDocs() {
544
544
  return count;
545
545
  }
546
546
 
547
- // Deploy to settings/docs/ (agent reference)
548
- const docsDest = join(workspacePath, 'settings', 'docs');
549
- const docsCount = renderTemplates(docsDest);
550
- if (docsCount > 0) {
551
- console.log(` + ${docsCount} personalized doc(s) deployed to ${docsDest.replace(HOME, '~')}/`);
552
- }
553
-
554
- // Deploy to library/documentation/ (human-readable library copy)
547
+ // Deploy to library/documentation/ (the canonical doc path since Mar 28 rename).
548
+ // Previously also deployed to settings/docs/ which Parker renamed to library/documentation/.
549
+ // That created a ghost folder on every install. Removed 2026-04-05 per INST-1.
555
550
  const libraryDest = join(workspacePath, 'library', 'documentation');
556
- if (existsSync(join(workspacePath, 'library'))) {
557
- const libCount = renderTemplates(libraryDest);
558
- if (libCount > 0) {
559
- console.log(` + ${libCount} doc(s) deployed to ${libraryDest.replace(HOME, '~')}/`);
560
- }
551
+ mkdirSync(libraryDest, { recursive: true });
552
+ const docsCount = renderTemplates(libraryDest);
553
+ if (docsCount > 0) {
554
+ console.log(` + ${docsCount} personalized doc(s) deployed to ${libraryDest.replace(HOME, '~')}/`);
561
555
  }
562
556
 
563
557
  return docsCount;
@@ -820,10 +814,16 @@ async function cmdInit() {
820
814
  // Scaffold workspace output dirs if workspace is configured
821
815
  const workspace = config.workspace;
822
816
  if (workspace && existsSync(workspace)) {
823
- // Per-agent workspace dirs
824
- const agentNameMap = { 'cc-mini': 'cc-mini', 'cc-air': 'cc-air', 'oc-lesa-mini': 'Lēsa' };
817
+ // Per-agent workspace dirs.
818
+ // Resolve the team folder name from config.json agents[id].teamFolder
819
+ // so agents with unicode names or custom folder names don't get ghost
820
+ // folders created from their agent ID. Falls back to agent ID if no
821
+ // override is configured. Fixed 2026-04-05 per INST-1: previously
822
+ // hardcoded a map that only knew three agents and created ghost folders
823
+ // for any others.
825
824
  for (const agentId of agentList) {
826
- const teamName = agentNameMap[agentId] || agentId;
825
+ const agentObj = typeof agentsObj[agentId] === 'object' ? agentsObj[agentId] : {};
826
+ const teamName = agentObj.teamFolder || agentObj.name || agentId;
827
827
  for (const sub of ['journals', 'automated/memory/summaries/daily', 'automated/memory/summaries/weekly', 'automated/memory/summaries/monthly', 'automated/memory/summaries/quarterly']) {
828
828
  dirs.push(join(workspace, 'team', teamName, sub));
829
829
  }
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.19",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {