@wipcomputer/wip-ldm-os 0.4.52 → 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.52"
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 ?? isCore,
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,66 +732,57 @@ function installClaudeCodeHook(repoPath, door) {
650
732
  }
651
733
 
652
734
  function installSkill(repoPath, toolName) {
653
- const 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 (existsSync(ocSkillDest)) {
658
- try {
659
- const srcContent = readFileSync(skillSrc, 'utf8');
660
- const destContent = readFileSync(ocSkillDest, 'utf8');
661
- if (srcContent === destContent) {
662
- skip(`Skill: ${toolName} already deployed`);
663
- return true;
664
- }
665
- } catch {}
666
- }
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;
741
+ if (!existsSync(skillSrc)) return false;
742
+
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;
667
747
 
668
748
  if (DRY_RUN) {
669
- 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)`);
670
751
  return true;
671
752
  }
672
753
 
673
754
  try {
674
- // Deploy to OpenClaw (~/.openclaw/skills/)
675
- mkdirSync(ocSkillDir, { recursive: true });
676
- cpSync(skillSrc, ocSkillDest);
677
-
678
- // Deploy to Claude Code (~/.claude/skills/) - standard discovery path
679
- const ccSkillDir = join(HOME, '.claude', 'skills', toolName);
680
- if (existsSync(join(HOME, '.claude'))) {
681
- mkdirSync(ccSkillDir, { recursive: true });
682
- cpSync(skillSrc, join(ccSkillDir, 'SKILL.md'));
683
- }
684
-
685
- ok(`Skill: deployed to ~/.openclaw/skills/ and ~/.claude/skills/`);
686
-
687
- // Deploy references/ if it exists (Agent Skills Spec pattern)
688
- const refsSrc = join(repoPath, 'references');
689
- if (existsSync(refsSrc)) {
690
- // To OpenClaw skill dir
691
- cpSync(refsSrc, join(ocSkillDir, 'references'), { recursive: true });
692
- // To Claude Code skill dir
693
- if (existsSync(ccSkillDir)) {
694
- cpSync(refsSrc, join(ccSkillDir, 'references'), { recursive: true });
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 });
763
+ }
764
+
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 });
695
773
  }
774
+ deployed.push(name);
775
+ }
696
776
 
697
- // Also deploy to home (settings/docs/skills/) so all agents can read them
698
- try {
699
- const ldmConfigPath = join(LDM_ROOT, 'config.json');
700
- if (existsSync(ldmConfigPath)) {
701
- const ldmConfig = JSON.parse(readFileSync(ldmConfigPath, 'utf8'));
702
- const workspace = (ldmConfig.workspace || '').replace('~', HOME);
703
- if (workspace && existsSync(workspace)) {
704
- const homeRefsDest = join(workspace, 'settings', 'docs', 'skills', toolName);
705
- mkdirSync(homeRefsDest, { recursive: true });
706
- cpSync(refsSrc, homeRefsDest, { recursive: true });
707
- ok(`Skill: references/ deployed to ${homeRefsDest.replace(HOME, '~')}`);
708
- }
709
- }
710
- } 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');
711
783
  }
712
784
 
785
+ ok(`Skill: ${toolName} deployed to ${deployed.join(', ')}`);
713
786
  return true;
714
787
  } catch (e) {
715
788
  fail(`Skill: deploy failed. ${e.message}`);
@@ -1016,4 +1089,4 @@ export function disableExtension(name) {
1016
1089
 
1017
1090
  // ── Exports for ldm CLI ──
1018
1091
 
1019
- 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.52",
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": {