@wipcomputer/wip-ldm-os 0.4.53 → 0.4.54

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/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.53"
12
+ version: "0.4.54"
13
13
  homepage: "https://github.com/wipcomputer/wip-ldm-os"
14
14
  author: "Parker Todd Brooks"
15
15
  category: infrastructure
package/bin/ldm.js CHANGED
@@ -521,6 +521,16 @@ async function cmdInit() {
521
521
  }
522
522
  }
523
523
 
524
+ // Detect installed harnesses (CC, OC, Codex, Cursor, Claude macOS)
525
+ try {
526
+ const { detectHarnesses } = await import('../lib/deploy.mjs');
527
+ const { harnesses } = detectHarnesses();
528
+ const detected = Object.entries(harnesses).filter(([,h]) => h.detected).map(([name]) => name);
529
+ if (detected.length > 0) {
530
+ console.log(` + Harnesses detected: ${detected.join(', ')}`);
531
+ }
532
+ } catch {}
533
+
524
534
  console.log('');
525
535
  console.log(` LDM OS v${PKG_VERSION} initialized at ${LDM_ROOT}`);
526
536
  console.log('');
@@ -627,9 +637,12 @@ async function cmdInstall() {
627
637
  cmdInit();
628
638
  }
629
639
 
630
- const { setFlags, installFromPath, installSingleTool, installToolbox } = await import('../lib/deploy.mjs');
640
+ const { setFlags, installFromPath, installSingleTool, installToolbox, detectHarnesses } = await import('../lib/deploy.mjs');
631
641
  const { detectInterfacesJSON } = await import('../lib/detect.mjs');
632
642
 
643
+ // Refresh harness detection (catches newly installed harnesses)
644
+ detectHarnesses();
645
+
633
646
  setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT });
634
647
 
635
648
  // --help flag (#81)
package/lib/deploy.mjs CHANGED
@@ -76,9 +76,91 @@ function saveRegistry(registry) {
76
76
  writeJSON(REGISTRY_PATH, registry);
77
77
  }
78
78
 
79
- // Core extensions are always enabled. Everything else defaults to disabled on first install.
79
+ // Core extensions are always enabled.
80
80
  const CORE_EXTENSIONS = new Set(['memory-crystal']);
81
81
 
82
+ // ── Harness Detection ──
83
+
84
+ const LDM_CONFIG_PATH = join(LDM_ROOT, 'config.json');
85
+
86
+ /**
87
+ * Detect which AI harnesses are installed on this system.
88
+ * Writes results to ~/.ldm/config.json so the installer knows where to deploy.
89
+ */
90
+ export function detectHarnesses() {
91
+ const harnesses = {};
92
+
93
+ // Claude Code CLI
94
+ const claudeHome = join(HOME, '.claude');
95
+ harnesses['claude-code'] = {
96
+ detected: existsSync(claudeHome),
97
+ home: claudeHome,
98
+ skills: join(claudeHome, 'skills'),
99
+ rules: join(claudeHome, 'rules'),
100
+ settings: join(claudeHome, 'settings.json'),
101
+ };
102
+
103
+ // Claude macOS app
104
+ const claudeMacHome = join(HOME, 'Library', 'Application Support', 'Claude');
105
+ harnesses['claude-macos'] = {
106
+ detected: existsSync(claudeMacHome),
107
+ home: claudeMacHome,
108
+ };
109
+
110
+ // OpenClaw (Lesa)
111
+ harnesses['openclaw'] = {
112
+ detected: existsSync(OC_ROOT),
113
+ home: OC_ROOT,
114
+ skills: join(OC_ROOT, 'skills'),
115
+ extensions: OC_EXTENSIONS,
116
+ };
117
+
118
+ // Codex
119
+ const codexHome = join(HOME, '.codex');
120
+ harnesses['codex'] = {
121
+ detected: existsSync(codexHome),
122
+ home: codexHome,
123
+ skills: join(codexHome, 'skills'),
124
+ };
125
+
126
+ // Cursor
127
+ const cursorHome = join(HOME, '.cursor');
128
+ harnesses['cursor'] = {
129
+ detected: existsSync(cursorHome),
130
+ home: cursorHome,
131
+ };
132
+
133
+ // Read workspace from existing config
134
+ let workspace = '';
135
+ try {
136
+ const existing = readJSON(LDM_CONFIG_PATH) || {};
137
+ workspace = (existing.workspace || '').replace('~', HOME);
138
+ } catch {}
139
+
140
+ // Save to config
141
+ try {
142
+ const existing = readJSON(LDM_CONFIG_PATH) || {};
143
+ existing.harnesses = harnesses;
144
+ writeJSON(LDM_CONFIG_PATH, existing);
145
+ } catch {}
146
+
147
+ return { harnesses, workspace };
148
+ }
149
+
150
+ /**
151
+ * Get detected harnesses from config. Runs detection if not cached.
152
+ */
153
+ function getHarnesses() {
154
+ try {
155
+ const config = readJSON(LDM_CONFIG_PATH) || {};
156
+ if (config.harnesses) {
157
+ const workspace = (config.workspace || '').replace('~', HOME);
158
+ return { harnesses: config.harnesses, workspace };
159
+ }
160
+ } catch {}
161
+ return detectHarnesses();
162
+ }
163
+
82
164
  function updateRegistry(name, info) {
83
165
  const registry = loadRegistry();
84
166
  const existing = registry.extensions[name];
@@ -86,7 +168,7 @@ function updateRegistry(name, info) {
86
168
  registry.extensions[name] = {
87
169
  ...existing,
88
170
  ...info,
89
- enabled: existing?.enabled ?? true,
171
+ enabled: existing?.enabled ?? true, // New installs are enabled by default. User runs ldm disable to turn off.
90
172
  updatedAt: new Date().toISOString(),
91
173
  };
92
174
  saveRegistry(registry);
@@ -650,76 +732,57 @@ function installClaudeCodeHook(repoPath, door) {
650
732
  }
651
733
 
652
734
  function installSkill(repoPath, toolName) {
653
- let skillSrc = join(repoPath, 'SKILL.md');
654
- const ocSkillDir = join(OC_ROOT, 'skills', toolName);
655
- const ocSkillDest = join(ocSkillDir, 'SKILL.md');
735
+ const { harnesses, workspace } = getHarnesses();
656
736
 
657
- // If the original source is gone (tmp clone cleaned up), use the already-deployed OC copy
658
- if (!existsSync(skillSrc) && existsSync(ocSkillDest)) {
659
- skillSrc = ocSkillDest;
660
- }
737
+ // Find SKILL.md source: repo path first, then permanent copy at ~/.ldm/extensions/
738
+ let skillSrc = join(repoPath, 'SKILL.md');
739
+ const permanentSkill = join(LDM_EXTENSIONS, toolName, 'SKILL.md');
740
+ if (!existsSync(skillSrc) && existsSync(permanentSkill)) skillSrc = permanentSkill;
661
741
  if (!existsSync(skillSrc)) return false;
662
742
 
663
- if (existsSync(ocSkillDest)) {
664
- try {
665
- const srcContent = readFileSync(skillSrc, 'utf8');
666
- const destContent = readFileSync(ocSkillDest, 'utf8');
667
- if (srcContent === destContent) {
668
- skip(`Skill: ${toolName} already deployed`);
669
- return true;
670
- }
671
- } catch {}
672
- }
743
+ // Find references/ source: repo path first, then permanent copy
744
+ let refsSrc = join(repoPath, 'references');
745
+ const permanentRefs = join(LDM_EXTENSIONS, toolName, 'references');
746
+ if (!existsSync(refsSrc) && existsSync(permanentRefs)) refsSrc = permanentRefs;
673
747
 
674
748
  if (DRY_RUN) {
675
- ok(`Skill: would deploy ${toolName}/SKILL.md (dry run)`);
749
+ const targets = Object.entries(harnesses).filter(([,h]) => h.detected && h.skills).map(([name]) => name);
750
+ ok(`Skill: would deploy ${toolName} to ${targets.join(', ')} (dry run)`);
676
751
  return true;
677
752
  }
678
753
 
679
754
  try {
680
- // Deploy to OpenClaw (~/.openclaw/skills/)
681
- mkdirSync(ocSkillDir, { recursive: true });
682
- cpSync(skillSrc, ocSkillDest);
683
-
684
- // Deploy to Claude Code (~/.claude/skills/) - standard discovery path
685
- const ccSkillDir = join(HOME, '.claude', 'skills', toolName);
686
- if (existsSync(join(HOME, '.claude'))) {
687
- mkdirSync(ccSkillDir, { recursive: true });
688
- cpSync(skillSrc, join(ccSkillDir, 'SKILL.md'));
755
+ const deployed = [];
756
+
757
+ // 1. Save permanent copy to ~/.ldm/extensions/<name>/ (survives tmp cleanup)
758
+ const ldmSkillDir = join(LDM_EXTENSIONS, toolName);
759
+ mkdirSync(ldmSkillDir, { recursive: true });
760
+ cpSync(skillSrc, join(ldmSkillDir, 'SKILL.md'));
761
+ if (existsSync(refsSrc) && refsSrc !== permanentRefs) {
762
+ cpSync(refsSrc, join(ldmSkillDir, 'references'), { recursive: true });
689
763
  }
690
764
 
691
- ok(`Skill: deployed to ~/.openclaw/skills/ and ~/.claude/skills/`);
692
-
693
- // Deploy references/ if it exists (Agent Skills Spec pattern)
694
- let refsSrc = join(repoPath, 'references');
695
- // Fallback to already-deployed OC copy if source is gone
696
- if (!existsSync(refsSrc) && existsSync(join(ocSkillDir, 'references'))) {
697
- refsSrc = join(ocSkillDir, 'references');
698
- }
699
- if (existsSync(refsSrc)) {
700
- // To OpenClaw skill dir
701
- cpSync(refsSrc, join(ocSkillDir, 'references'), { recursive: true });
702
- // To Claude Code skill dir
703
- if (existsSync(ccSkillDir)) {
704
- cpSync(refsSrc, join(ccSkillDir, 'references'), { recursive: true });
765
+ // 2. Deploy to every detected harness that has a skills path
766
+ for (const [name, harness] of Object.entries(harnesses)) {
767
+ if (!harness.detected || !harness.skills) continue;
768
+ const dest = join(harness.skills, toolName);
769
+ mkdirSync(dest, { recursive: true });
770
+ cpSync(skillSrc, join(dest, 'SKILL.md'));
771
+ if (existsSync(refsSrc)) {
772
+ cpSync(refsSrc, join(dest, 'references'), { recursive: true });
705
773
  }
774
+ deployed.push(name);
775
+ }
706
776
 
707
- // Also deploy to home (settings/docs/skills/) so all agents can read them
708
- try {
709
- const ldmConfigPath = join(LDM_ROOT, 'config.json');
710
- if (existsSync(ldmConfigPath)) {
711
- const ldmConfig = JSON.parse(readFileSync(ldmConfigPath, 'utf8'));
712
- const workspace = (ldmConfig.workspace || '').replace('~', HOME);
713
- if (workspace && existsSync(workspace)) {
714
- const homeRefsDest = join(workspace, 'settings', 'docs', 'skills', toolName);
715
- mkdirSync(homeRefsDest, { recursive: true });
716
- cpSync(refsSrc, homeRefsDest, { recursive: true });
717
- ok(`Skill: references/ deployed to ${homeRefsDest.replace(HOME, '~')}`);
718
- }
719
- }
720
- } catch {}
777
+ // 3. Deploy references/ to home (workspace settings/docs/skills/)
778
+ if (existsSync(refsSrc) && workspace && existsSync(workspace)) {
779
+ const homeRefsDest = join(workspace, 'settings', 'docs', 'skills', toolName);
780
+ mkdirSync(homeRefsDest, { recursive: true });
781
+ cpSync(refsSrc, homeRefsDest, { recursive: true });
782
+ deployed.push('home');
721
783
  }
722
784
 
785
+ ok(`Skill: ${toolName} deployed to ${deployed.join(', ')}`);
723
786
  return true;
724
787
  } catch (e) {
725
788
  fail(`Skill: deploy failed. ${e.message}`);
@@ -1026,4 +1089,4 @@ export function disableExtension(name) {
1026
1089
 
1027
1090
  // ── Exports for ldm CLI ──
1028
1091
 
1029
- export { loadRegistry, saveRegistry, updateRegistry, readJSON, writeJSON, runBuildIfNeeded, CORE_EXTENSIONS };
1092
+ export { loadRegistry, saveRegistry, updateRegistry, readJSON, writeJSON, runBuildIfNeeded, detectHarnesses, CORE_EXTENSIONS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.53",
3
+ "version": "0.4.54",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {