@wipcomputer/wip-ldm-os 0.4.29 → 0.4.31
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 +1 -1
- package/bin/ldm.js +363 -30
- package/package.json +1 -1
package/SKILL.md
CHANGED
package/bin/ldm.js
CHANGED
|
@@ -30,6 +30,7 @@ const LDM_ROOT = join(HOME, '.ldm');
|
|
|
30
30
|
const LDM_EXTENSIONS = join(LDM_ROOT, 'extensions');
|
|
31
31
|
const VERSION_PATH = join(LDM_ROOT, 'version.json');
|
|
32
32
|
const REGISTRY_PATH = join(LDM_EXTENSIONS, 'registry.json');
|
|
33
|
+
const LDM_TMP = join(LDM_ROOT, 'tmp');
|
|
33
34
|
|
|
34
35
|
// Install log (#101): append to ~/.ldm/logs/install.log
|
|
35
36
|
import { appendFileSync } from 'node:fs';
|
|
@@ -227,7 +228,21 @@ function loadCatalog() {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
function findInCatalog(id) {
|
|
230
|
-
|
|
231
|
+
const q = id.toLowerCase();
|
|
232
|
+
const catalog = loadCatalog();
|
|
233
|
+
// Exact id match
|
|
234
|
+
const exact = catalog.find(c => c.id === id);
|
|
235
|
+
if (exact) return exact;
|
|
236
|
+
// Partial id match (e.g. "xai-grok" matches "wip-xai-grok")
|
|
237
|
+
const partial = catalog.find(c => c.id.toLowerCase().includes(q) || q.includes(c.id.toLowerCase()));
|
|
238
|
+
if (partial) return partial;
|
|
239
|
+
// Name match (case-insensitive, e.g. "xAI Grok")
|
|
240
|
+
const byName = catalog.find(c => c.name && c.name.toLowerCase() === q);
|
|
241
|
+
if (byName) return byName;
|
|
242
|
+
// registryMatches match
|
|
243
|
+
const byRegistry = catalog.find(c => (c.registryMatches || []).some(m => m.toLowerCase() === q));
|
|
244
|
+
if (byRegistry) return byRegistry;
|
|
245
|
+
return null;
|
|
231
246
|
}
|
|
232
247
|
|
|
233
248
|
// ── ldm init ──
|
|
@@ -484,8 +499,22 @@ async function cmdInstall() {
|
|
|
484
499
|
return cmdInstallCatalog();
|
|
485
500
|
}
|
|
486
501
|
|
|
502
|
+
// If target is a private repo (org/name-private), redirect to public (#134)
|
|
503
|
+
let resolvedTarget = target;
|
|
504
|
+
if (target.match(/^[\w-]+\/[\w.-]+-private$/) && !existsSync(resolve(target))) {
|
|
505
|
+
const publicRepo = target.replace(/-private$/, '');
|
|
506
|
+
const catalogHit = findInCatalog(basename(publicRepo));
|
|
507
|
+
if (catalogHit) {
|
|
508
|
+
console.log(` Redirecting ${target} to public repo: ${catalogHit.repo}`);
|
|
509
|
+
resolvedTarget = catalogHit.repo;
|
|
510
|
+
} else {
|
|
511
|
+
console.log(` Redirecting ${target} to public repo: ${publicRepo}`);
|
|
512
|
+
resolvedTarget = publicRepo;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
487
516
|
// Check if target is a catalog ID (e.g. "memory-crystal")
|
|
488
|
-
const catalogEntry = findInCatalog(
|
|
517
|
+
const catalogEntry = findInCatalog(resolvedTarget);
|
|
489
518
|
if (catalogEntry) {
|
|
490
519
|
console.log('');
|
|
491
520
|
console.log(` Resolved "${target}" via catalog to ${catalogEntry.repo}`);
|
|
@@ -493,10 +522,11 @@ async function cmdInstall() {
|
|
|
493
522
|
// Use the repo field to clone from GitHub
|
|
494
523
|
const repoTarget = catalogEntry.repo;
|
|
495
524
|
const repoName = basename(repoTarget);
|
|
496
|
-
const repoPath = join(
|
|
525
|
+
const repoPath = join(LDM_TMP, `ldm-install-${repoName}`);
|
|
497
526
|
const httpsUrl = `https://github.com/${repoTarget}.git`;
|
|
498
527
|
const sshUrl = `git@github.com:${repoTarget}.git`;
|
|
499
528
|
|
|
529
|
+
mkdirSync(LDM_TMP, { recursive: true });
|
|
500
530
|
console.log(` Cloning ${repoTarget}...`);
|
|
501
531
|
try {
|
|
502
532
|
if (existsSync(repoPath)) {
|
|
@@ -523,8 +553,8 @@ async function cmdInstall() {
|
|
|
523
553
|
|
|
524
554
|
await installFromPath(repoPath);
|
|
525
555
|
|
|
526
|
-
// Clean up
|
|
527
|
-
if (!DRY_RUN &&
|
|
556
|
+
// Clean up staging clone after install (#32, #135)
|
|
557
|
+
if (!DRY_RUN && repoPath.startsWith(LDM_TMP)) {
|
|
528
558
|
try { execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' }); } catch {}
|
|
529
559
|
}
|
|
530
560
|
return;
|
|
@@ -534,10 +564,10 @@ async function cmdInstall() {
|
|
|
534
564
|
let repoPath;
|
|
535
565
|
|
|
536
566
|
// Check if target looks like an npm package (starts with @ or is a plain name without /)
|
|
537
|
-
if (
|
|
567
|
+
if (resolvedTarget.startsWith('@') || (!resolvedTarget.includes('/') && !existsSync(resolve(resolvedTarget)))) {
|
|
538
568
|
// Try npm install to temp dir
|
|
539
|
-
const npmName =
|
|
540
|
-
const tempDir = join(
|
|
569
|
+
const npmName = resolvedTarget;
|
|
570
|
+
const tempDir = join(LDM_TMP, `ldm-install-npm-${Date.now()}`);
|
|
541
571
|
console.log('');
|
|
542
572
|
console.log(` Installing ${npmName} from npm...`);
|
|
543
573
|
try {
|
|
@@ -560,19 +590,20 @@ async function cmdInstall() {
|
|
|
560
590
|
}
|
|
561
591
|
}
|
|
562
592
|
|
|
563
|
-
if (!repoPath && (
|
|
564
|
-
const isShorthand =
|
|
593
|
+
if (!repoPath && (resolvedTarget.startsWith('http') || resolvedTarget.startsWith('git@') || resolvedTarget.match(/^[\w-]+\/[\w.-]+$/))) {
|
|
594
|
+
const isShorthand = resolvedTarget.match(/^[\w-]+\/[\w.-]+$/);
|
|
565
595
|
const httpsUrl = isShorthand
|
|
566
|
-
? `https://github.com/${
|
|
567
|
-
:
|
|
596
|
+
? `https://github.com/${resolvedTarget}.git`
|
|
597
|
+
: resolvedTarget;
|
|
568
598
|
const sshUrl = isShorthand
|
|
569
|
-
? `git@github.com:${
|
|
570
|
-
:
|
|
599
|
+
? `git@github.com:${resolvedTarget}.git`
|
|
600
|
+
: resolvedTarget.replace(/^https:\/\/github\.com\//, 'git@github.com:');
|
|
571
601
|
const repoName = basename(httpsUrl).replace('.git', '');
|
|
572
|
-
repoPath = join(
|
|
602
|
+
repoPath = join(LDM_TMP, `ldm-install-${repoName}`);
|
|
573
603
|
|
|
604
|
+
mkdirSync(LDM_TMP, { recursive: true });
|
|
574
605
|
console.log('');
|
|
575
|
-
console.log(` Cloning ${isShorthand ?
|
|
606
|
+
console.log(` Cloning ${isShorthand ? resolvedTarget : httpsUrl}...`);
|
|
576
607
|
try {
|
|
577
608
|
if (existsSync(repoPath)) {
|
|
578
609
|
execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
|
|
@@ -590,7 +621,7 @@ async function cmdInstall() {
|
|
|
590
621
|
process.exit(1);
|
|
591
622
|
}
|
|
592
623
|
} else if (!repoPath) {
|
|
593
|
-
repoPath = resolve(
|
|
624
|
+
repoPath = resolve(resolvedTarget);
|
|
594
625
|
if (!existsSync(repoPath)) {
|
|
595
626
|
console.error(` x Path not found: ${repoPath}`);
|
|
596
627
|
process.exit(1);
|
|
@@ -605,8 +636,8 @@ async function cmdInstall() {
|
|
|
605
636
|
|
|
606
637
|
await installFromPath(repoPath);
|
|
607
638
|
|
|
608
|
-
// Clean up
|
|
609
|
-
if (!DRY_RUN &&
|
|
639
|
+
// Clean up staging clone after install (#32, #135)
|
|
640
|
+
if (!DRY_RUN && repoPath.startsWith(LDM_TMP)) {
|
|
610
641
|
try { execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' }); } catch {}
|
|
611
642
|
}
|
|
612
643
|
}
|
|
@@ -735,9 +766,51 @@ async function cmdInstallCatalog() {
|
|
|
735
766
|
console.log('');
|
|
736
767
|
}
|
|
737
768
|
|
|
769
|
+
// Clean ghost entries from registry (#134, #135)
|
|
770
|
+
if (registry?.extensions) {
|
|
771
|
+
const names = Object.keys(registry.extensions);
|
|
772
|
+
let cleaned = 0;
|
|
773
|
+
for (const name of names) {
|
|
774
|
+
// Remove -private duplicates (e.g. wip-xai-grok-private when wip-xai-grok exists)
|
|
775
|
+
const publicName = name.replace(/-private$/, '');
|
|
776
|
+
if (name !== publicName && registry.extensions[publicName]) {
|
|
777
|
+
delete registry.extensions[name];
|
|
778
|
+
cleaned++;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
// Remove ldm-install- prefixed entries (ghost from /tmp/ clones)
|
|
782
|
+
if (name.startsWith('ldm-install-')) {
|
|
783
|
+
const cleanName = name.replace(/^ldm-install-/, '');
|
|
784
|
+
delete registry.extensions[name];
|
|
785
|
+
cleaned++;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (cleaned > 0 && !DRY_RUN) {
|
|
789
|
+
writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
|
790
|
+
installLog(`Cleaned ${cleaned} ghost registry entries`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
738
794
|
// Build the update plan: check ALL installed extensions against npm (#55)
|
|
739
795
|
const npmUpdates = [];
|
|
740
796
|
|
|
797
|
+
// Check CLI self-update (#132)
|
|
798
|
+
try {
|
|
799
|
+
const cliLatest = execSync('npm view @wipcomputer/wip-ldm-os version 2>/dev/null', {
|
|
800
|
+
encoding: 'utf8', timeout: 10000,
|
|
801
|
+
}).trim();
|
|
802
|
+
if (cliLatest && cliLatest !== PKG_VERSION) {
|
|
803
|
+
npmUpdates.push({
|
|
804
|
+
name: 'LDM OS CLI',
|
|
805
|
+
catalogNpm: '@wipcomputer/wip-ldm-os',
|
|
806
|
+
currentVersion: PKG_VERSION,
|
|
807
|
+
latestVersion: cliLatest,
|
|
808
|
+
hasUpdate: true,
|
|
809
|
+
isCLI: true,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
} catch {}
|
|
813
|
+
|
|
741
814
|
// Check every installed extension against npm via catalog
|
|
742
815
|
console.log(' Checking npm for updates...');
|
|
743
816
|
for (const [name, entry] of Object.entries(reconciled)) {
|
|
@@ -819,17 +892,51 @@ async function cmdInstallCatalog() {
|
|
|
819
892
|
} catch {}
|
|
820
893
|
}
|
|
821
894
|
|
|
895
|
+
// Check parent packages for toolbox-style repos (#132)
|
|
896
|
+
// If sub-tools are installed but the parent npm package has a newer version,
|
|
897
|
+
// report the parent as needing an update (not the individual sub-tool).
|
|
898
|
+
const checkedNpm = new Set(npmUpdates.map(u => u.catalogNpm));
|
|
899
|
+
for (const comp of components) {
|
|
900
|
+
if (!comp.npm || checkedNpm.has(comp.npm)) continue;
|
|
901
|
+
if (!comp.registryMatches || comp.registryMatches.length === 0) continue;
|
|
902
|
+
|
|
903
|
+
// If any registryMatch is installed, check the parent package
|
|
904
|
+
const installedMatch = comp.registryMatches.find(m => reconciled[m]);
|
|
905
|
+
if (!installedMatch) continue;
|
|
906
|
+
|
|
907
|
+
const currentVersion = reconciled[installedMatch]?.ldmVersion || reconciled[installedMatch]?.ocVersion || '?';
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const latest = execSync(`npm view ${comp.npm} version 2>/dev/null`, {
|
|
911
|
+
encoding: 'utf8', timeout: 10000,
|
|
912
|
+
}).trim();
|
|
913
|
+
if (latest && latest !== currentVersion) {
|
|
914
|
+
// Remove any sub-tool entries that duplicate this parent
|
|
915
|
+
for (let i = npmUpdates.length - 1; i >= 0; i--) {
|
|
916
|
+
if (npmUpdates[i].catalogNpm === comp.npm && !npmUpdates[i].isCLI) {
|
|
917
|
+
npmUpdates.splice(i, 1);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
npmUpdates.push({
|
|
921
|
+
name: comp.id,
|
|
922
|
+
catalogRepo: comp.repo,
|
|
923
|
+
catalogNpm: comp.npm,
|
|
924
|
+
currentVersion,
|
|
925
|
+
latestVersion: latest,
|
|
926
|
+
hasUpdate: true,
|
|
927
|
+
isParent: true,
|
|
928
|
+
registryMatches: comp.registryMatches,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
} catch {}
|
|
932
|
+
checkedNpm.add(comp.npm);
|
|
933
|
+
}
|
|
934
|
+
|
|
822
935
|
const totalUpdates = npmUpdates.length;
|
|
823
936
|
|
|
824
937
|
if (DRY_RUN) {
|
|
825
938
|
// Summary block (#80)
|
|
826
|
-
const
|
|
827
|
-
try {
|
|
828
|
-
return execSync('npm view @wipcomputer/wip-ldm-os version 2>/dev/null', {
|
|
829
|
-
encoding: 'utf8', timeout: 10000,
|
|
830
|
-
}).trim();
|
|
831
|
-
} catch { return null; }
|
|
832
|
-
})();
|
|
939
|
+
const cliUpdate = npmUpdates.find(u => u.isCLI);
|
|
833
940
|
|
|
834
941
|
const agentDirs = (() => {
|
|
835
942
|
try {
|
|
@@ -848,8 +955,8 @@ async function cmdInstallCatalog() {
|
|
|
848
955
|
console.log('');
|
|
849
956
|
console.log(' Summary');
|
|
850
957
|
console.log(' ────────────────────────────────────');
|
|
851
|
-
if (
|
|
852
|
-
console.log(` LDM OS CLI v${PKG_VERSION} -> v${
|
|
958
|
+
if (cliUpdate) {
|
|
959
|
+
console.log(` LDM OS CLI v${PKG_VERSION} -> v${cliUpdate.latestVersion}`);
|
|
853
960
|
} else {
|
|
854
961
|
console.log(` LDM OS CLI v${PKG_VERSION} (latest)`);
|
|
855
962
|
}
|
|
@@ -918,13 +1025,21 @@ async function cmdInstallCatalog() {
|
|
|
918
1025
|
}
|
|
919
1026
|
} catch {}
|
|
920
1027
|
|
|
921
|
-
// Check orphaned /tmp/
|
|
1028
|
+
// Check orphaned staging dirs (old /tmp/ and new ~/.ldm/tmp/)
|
|
922
1029
|
try {
|
|
923
1030
|
const tmpCount = readdirSync('/private/tmp').filter(d => d.startsWith('ldm-install-')).length;
|
|
924
1031
|
if (tmpCount > 0) {
|
|
925
1032
|
healthIssues.push(` ! ${tmpCount} orphaned /tmp/ldm-install-* dirs (would clean up)`);
|
|
926
1033
|
}
|
|
927
1034
|
} catch {}
|
|
1035
|
+
try {
|
|
1036
|
+
if (existsSync(LDM_TMP)) {
|
|
1037
|
+
const ldmTmpCount = readdirSync(LDM_TMP).filter(d => d.startsWith('ldm-install-')).length;
|
|
1038
|
+
if (ldmTmpCount > 0) {
|
|
1039
|
+
healthIssues.push(` ! ${ldmTmpCount} orphaned ~/.ldm/tmp/ldm-install-* dirs (would clean up)`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
} catch {}
|
|
928
1043
|
|
|
929
1044
|
if (healthIssues.length > 0) {
|
|
930
1045
|
console.log('');
|
|
@@ -1075,7 +1190,7 @@ async function cmdInstallCatalog() {
|
|
|
1075
1190
|
}
|
|
1076
1191
|
} catch {}
|
|
1077
1192
|
|
|
1078
|
-
// 3. Clean orphaned /tmp/ldm
|
|
1193
|
+
// 3. Clean orphaned staging dirs (old /tmp/ and new ~/.ldm/tmp/)
|
|
1079
1194
|
try {
|
|
1080
1195
|
const tmpDirs = readdirSync('/private/tmp').filter(d => d.startsWith('ldm-install-'));
|
|
1081
1196
|
if (tmpDirs.length > 0) {
|
|
@@ -1087,6 +1202,19 @@ async function cmdInstallCatalog() {
|
|
|
1087
1202
|
console.log(` + Cleaned ${tmpDirs.length} orphaned /tmp/ clone(s)`);
|
|
1088
1203
|
}
|
|
1089
1204
|
} catch {}
|
|
1205
|
+
try {
|
|
1206
|
+
if (existsSync(LDM_TMP)) {
|
|
1207
|
+
const ldmTmpDirs = readdirSync(LDM_TMP).filter(d => d.startsWith('ldm-install-'));
|
|
1208
|
+
if (ldmTmpDirs.length > 0) {
|
|
1209
|
+
console.log(` Cleaning ${ldmTmpDirs.length} orphaned ~/.ldm/tmp/ dirs...`);
|
|
1210
|
+
for (const d of ldmTmpDirs) {
|
|
1211
|
+
try { execSync(`rm -rf "${join(LDM_TMP, d)}"`, { stdio: 'pipe', timeout: 10000 }); } catch {}
|
|
1212
|
+
}
|
|
1213
|
+
healthFixes++;
|
|
1214
|
+
console.log(` + Cleaned ${ldmTmpDirs.length} orphaned ~/.ldm/tmp/ clone(s)`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
} catch {}
|
|
1090
1218
|
|
|
1091
1219
|
if (healthFixes > 0) {
|
|
1092
1220
|
console.log(` ${healthFixes} health issue(s) fixed.`);
|
|
@@ -2036,6 +2164,208 @@ async function main() {
|
|
|
2036
2164
|
console.log('');
|
|
2037
2165
|
}
|
|
2038
2166
|
|
|
2167
|
+
// ── ldm uninstall (#114) ──
|
|
2168
|
+
|
|
2169
|
+
async function cmdUninstall() {
|
|
2170
|
+
const keepData = !args.includes('--all');
|
|
2171
|
+
const isDryRun = args.includes('--dry-run');
|
|
2172
|
+
|
|
2173
|
+
console.log('');
|
|
2174
|
+
console.log(' LDM OS Uninstall');
|
|
2175
|
+
console.log(' ────────────────────────────────────');
|
|
2176
|
+
|
|
2177
|
+
if (keepData) {
|
|
2178
|
+
console.log(' Your data will be PRESERVED:');
|
|
2179
|
+
console.log(' ~/.ldm/memory/ (crystal.db, shared memory)');
|
|
2180
|
+
console.log(' ~/.ldm/agents/ (identity, journals, daily logs)');
|
|
2181
|
+
console.log('');
|
|
2182
|
+
console.log(' Use --all to remove everything including data.');
|
|
2183
|
+
} else {
|
|
2184
|
+
console.log(' WARNING: --all flag set. ALL data will be removed.');
|
|
2185
|
+
console.log(' This includes crystal.db, agent files, journals, everything.');
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
console.log('');
|
|
2189
|
+
console.log(' Will remove:');
|
|
2190
|
+
|
|
2191
|
+
// 1. MCP servers
|
|
2192
|
+
const claudeJsonPath = join(HOME, '.claude.json');
|
|
2193
|
+
try {
|
|
2194
|
+
const claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
2195
|
+
const mcpNames = Object.keys(claudeJson.mcpServers || {}).filter(n =>
|
|
2196
|
+
n.includes('crystal') || n.includes('wip-') || n.includes('memory') ||
|
|
2197
|
+
n.includes('grok') || n.includes('lesa') || n.includes('1password')
|
|
2198
|
+
);
|
|
2199
|
+
if (mcpNames.length > 0) {
|
|
2200
|
+
console.log(` MCP servers: ${mcpNames.join(', ')}`);
|
|
2201
|
+
}
|
|
2202
|
+
} catch {}
|
|
2203
|
+
|
|
2204
|
+
// 2. CC hooks
|
|
2205
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
2206
|
+
try {
|
|
2207
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
2208
|
+
let hookCount = 0;
|
|
2209
|
+
for (const [event, entries] of Object.entries(settings.hooks || {})) {
|
|
2210
|
+
for (const entry of (Array.isArray(entries) ? entries : [])) {
|
|
2211
|
+
for (const h of (entry.hooks || [])) {
|
|
2212
|
+
if (h.command?.includes('.ldm') || h.command?.includes('wip-')) hookCount++;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (hookCount > 0) console.log(` CC hooks: ${hookCount} hook(s)`);
|
|
2217
|
+
} catch {}
|
|
2218
|
+
|
|
2219
|
+
// 3. Skills
|
|
2220
|
+
const skillsDir = join(HOME, '.openclaw', 'skills');
|
|
2221
|
+
try {
|
|
2222
|
+
const skills = readdirSync(skillsDir).filter(d => d !== '.DS_Store');
|
|
2223
|
+
if (skills.length > 0) console.log(` Skills: ${skills.join(', ')}`);
|
|
2224
|
+
} catch {}
|
|
2225
|
+
|
|
2226
|
+
// 4. Cron jobs
|
|
2227
|
+
try {
|
|
2228
|
+
const crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
|
|
2229
|
+
const ldmLines = crontab.split('\n').filter(l => l.includes('.ldm') || l.includes('crystal-capture') || l.includes('process-monitor'));
|
|
2230
|
+
if (ldmLines.length > 0) console.log(` Cron jobs: ${ldmLines.length}`);
|
|
2231
|
+
} catch {}
|
|
2232
|
+
|
|
2233
|
+
// 5. Global npm packages
|
|
2234
|
+
try {
|
|
2235
|
+
const npmList = execSync('npm list -g --depth=0 --json 2>/dev/null', { encoding: 'utf8' });
|
|
2236
|
+
const deps = JSON.parse(npmList).dependencies || {};
|
|
2237
|
+
const wipPkgs = Object.keys(deps).filter(n => n.startsWith('@wipcomputer/'));
|
|
2238
|
+
if (wipPkgs.length > 0) console.log(` npm packages: ${wipPkgs.join(', ')}`);
|
|
2239
|
+
} catch {}
|
|
2240
|
+
|
|
2241
|
+
// 6. Directories
|
|
2242
|
+
console.log(` ~/.ldm/extensions/`);
|
|
2243
|
+
if (!keepData) {
|
|
2244
|
+
console.log(` ~/.ldm/memory/`);
|
|
2245
|
+
console.log(` ~/.ldm/agents/`);
|
|
2246
|
+
}
|
|
2247
|
+
console.log(` ~/.ldm/state/, bin/, hooks/, logs/, sessions/, messages/`);
|
|
2248
|
+
|
|
2249
|
+
if (isDryRun) {
|
|
2250
|
+
console.log('');
|
|
2251
|
+
console.log(' Dry run. Nothing removed.');
|
|
2252
|
+
console.log('');
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Confirm
|
|
2257
|
+
if (process.stdin.isTTY) {
|
|
2258
|
+
const { createInterface } = await import('readline');
|
|
2259
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2260
|
+
const answer = await new Promise(resolve => {
|
|
2261
|
+
rl.question('\n Type "uninstall" to confirm: ', resolve);
|
|
2262
|
+
});
|
|
2263
|
+
rl.close();
|
|
2264
|
+
if (answer.trim() !== 'uninstall') {
|
|
2265
|
+
console.log(' Cancelled.');
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
console.log('');
|
|
2271
|
+
console.log(' Removing...');
|
|
2272
|
+
|
|
2273
|
+
// 1. Unregister MCP servers
|
|
2274
|
+
try {
|
|
2275
|
+
const claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
2276
|
+
const mcpNames = Object.keys(claudeJson.mcpServers || {}).filter(n =>
|
|
2277
|
+
n.includes('crystal') || n.includes('wip-') || n.includes('memory') ||
|
|
2278
|
+
n.includes('grok') || n.includes('lesa') || n.includes('1password')
|
|
2279
|
+
);
|
|
2280
|
+
for (const name of mcpNames) {
|
|
2281
|
+
delete claudeJson.mcpServers[name];
|
|
2282
|
+
}
|
|
2283
|
+
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + '\n');
|
|
2284
|
+
console.log(` + Removed ${mcpNames.length} MCP server(s)`);
|
|
2285
|
+
} catch {}
|
|
2286
|
+
|
|
2287
|
+
// 2. Remove CC hooks
|
|
2288
|
+
try {
|
|
2289
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
2290
|
+
let removed = 0;
|
|
2291
|
+
for (const [event, entries] of Object.entries(settings.hooks || {})) {
|
|
2292
|
+
if (!Array.isArray(entries)) continue;
|
|
2293
|
+
for (const entry of entries) {
|
|
2294
|
+
if (!entry.hooks) continue;
|
|
2295
|
+
const before = entry.hooks.length;
|
|
2296
|
+
entry.hooks = entry.hooks.filter(h => !h.command?.includes('.ldm') && !h.command?.includes('wip-'));
|
|
2297
|
+
removed += before - entry.hooks.length;
|
|
2298
|
+
}
|
|
2299
|
+
settings.hooks[event] = entries.filter(e => e.hooks?.length > 0);
|
|
2300
|
+
}
|
|
2301
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
2302
|
+
console.log(` + Removed ${removed} CC hook(s)`);
|
|
2303
|
+
} catch {}
|
|
2304
|
+
|
|
2305
|
+
// 3. Remove skills
|
|
2306
|
+
try {
|
|
2307
|
+
const skills = readdirSync(skillsDir).filter(d => d !== '.DS_Store');
|
|
2308
|
+
for (const s of skills) {
|
|
2309
|
+
execSync(`rm -rf "${join(skillsDir, s)}"`, { stdio: 'pipe' });
|
|
2310
|
+
}
|
|
2311
|
+
console.log(` + Removed ${skills.length} skill(s)`);
|
|
2312
|
+
} catch {}
|
|
2313
|
+
|
|
2314
|
+
// 4. Remove cron jobs
|
|
2315
|
+
try {
|
|
2316
|
+
const crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
|
|
2317
|
+
const filtered = crontab.split('\n').filter(l =>
|
|
2318
|
+
!l.includes('.ldm') && !l.includes('crystal-capture') && !l.includes('process-monitor')
|
|
2319
|
+
).join('\n');
|
|
2320
|
+
execSync(`echo "${filtered}" | crontab -`, { stdio: 'pipe' });
|
|
2321
|
+
console.log(' + Cleaned cron jobs');
|
|
2322
|
+
} catch {}
|
|
2323
|
+
|
|
2324
|
+
// 5. Remove npm packages
|
|
2325
|
+
try {
|
|
2326
|
+
const npmList = execSync('npm list -g --depth=0 --json 2>/dev/null', { encoding: 'utf8' });
|
|
2327
|
+
const deps = JSON.parse(npmList).dependencies || {};
|
|
2328
|
+
const wipPkgs = Object.keys(deps).filter(n => n.startsWith('@wipcomputer/'));
|
|
2329
|
+
for (const pkg of wipPkgs) {
|
|
2330
|
+
if (pkg === '@wipcomputer/wip-ldm-os') continue; // uninstall self last
|
|
2331
|
+
try { execSync(`npm uninstall -g ${pkg}`, { stdio: 'pipe', timeout: 30000 }); } catch {}
|
|
2332
|
+
}
|
|
2333
|
+
console.log(` + Removed ${wipPkgs.length - 1} npm package(s)`);
|
|
2334
|
+
} catch {}
|
|
2335
|
+
|
|
2336
|
+
// 6. Remove directories
|
|
2337
|
+
const dirsToRemove = ['extensions', 'state', 'bin', 'hooks', 'logs', 'sessions', 'messages', 'shared', '_trash'];
|
|
2338
|
+
if (!keepData) {
|
|
2339
|
+
dirsToRemove.push('memory', 'agents', 'secrets', 'backups');
|
|
2340
|
+
}
|
|
2341
|
+
for (const dir of dirsToRemove) {
|
|
2342
|
+
const p = join(LDM_ROOT, dir);
|
|
2343
|
+
if (existsSync(p)) {
|
|
2344
|
+
execSync(`rm -rf "${p}"`, { stdio: 'pipe' });
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
// Remove config and version files
|
|
2348
|
+
for (const f of ['version.json', 'config.json']) {
|
|
2349
|
+
const p = join(LDM_ROOT, f);
|
|
2350
|
+
if (existsSync(p)) unlinkSync(p);
|
|
2351
|
+
}
|
|
2352
|
+
console.log(' + Removed ~/.ldm/ contents');
|
|
2353
|
+
|
|
2354
|
+
if (keepData) {
|
|
2355
|
+
console.log('');
|
|
2356
|
+
console.log(' Preserved:');
|
|
2357
|
+
console.log(' ~/.ldm/memory/ (your data)');
|
|
2358
|
+
console.log(' ~/.ldm/agents/ (identity + journals)');
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// 7. Self-uninstall
|
|
2362
|
+
console.log('');
|
|
2363
|
+
console.log(' To finish, run: npm uninstall -g @wipcomputer/wip-ldm-os');
|
|
2364
|
+
console.log('');
|
|
2365
|
+
console.log(' LDM OS uninstalled.');
|
|
2366
|
+
console.log('');
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2039
2369
|
if (command === '--version' || command === '-v') {
|
|
2040
2370
|
console.log(PKG_VERSION);
|
|
2041
2371
|
process.exit(0);
|
|
@@ -2079,6 +2409,9 @@ async function main() {
|
|
|
2079
2409
|
case 'disable':
|
|
2080
2410
|
await cmdDisable();
|
|
2081
2411
|
break;
|
|
2412
|
+
case 'uninstall':
|
|
2413
|
+
await cmdUninstall();
|
|
2414
|
+
break;
|
|
2082
2415
|
default:
|
|
2083
2416
|
console.error(` Unknown command: ${command}`);
|
|
2084
2417
|
console.error(` Run: ldm --help`);
|