@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.
Files changed (3) hide show
  1. package/SKILL.md +1 -1
  2. package/bin/ldm.js +363 -30
  3. package/package.json +1 -1
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ license: MIT
5
5
  interface: [cli, skill]
6
6
  metadata:
7
7
  display-name: "LDM OS"
8
- version: "0.4.29"
8
+ version: "0.4.31"
9
9
  homepage: "https://github.com/wipcomputer/wip-ldm-os"
10
10
  author: "Parker Todd Brooks"
11
11
  category: infrastructure
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
- return loadCatalog().find(c => c.id === id);
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(target);
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('/tmp', `ldm-install-${repoName}`);
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 /tmp/ clone after install (#32)
527
- if (!DRY_RUN && (repoPath.startsWith('/tmp/') || repoPath.startsWith('/private/tmp/'))) {
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 (target.startsWith('@') || (!target.includes('/') && !existsSync(resolve(target)))) {
567
+ if (resolvedTarget.startsWith('@') || (!resolvedTarget.includes('/') && !existsSync(resolve(resolvedTarget)))) {
538
568
  // Try npm install to temp dir
539
- const npmName = target;
540
- const tempDir = join('/tmp', `ldm-install-npm-${Date.now()}`);
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 && (target.startsWith('http') || target.startsWith('git@') || target.match(/^[\w-]+\/[\w.-]+$/))) {
564
- const isShorthand = target.match(/^[\w-]+\/[\w.-]+$/);
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/${target}.git`
567
- : target;
596
+ ? `https://github.com/${resolvedTarget}.git`
597
+ : resolvedTarget;
568
598
  const sshUrl = isShorthand
569
- ? `git@github.com:${target}.git`
570
- : target.replace(/^https:\/\/github\.com\//, 'git@github.com:');
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('/tmp', `ldm-install-${repoName}`);
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 ? target : httpsUrl}...`);
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(target);
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 /tmp/ clone after install (#32)
609
- if (!DRY_RUN && (repoPath.startsWith('/tmp/') || repoPath.startsWith('/private/tmp/'))) {
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 cliLatest = (() => {
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 (cliLatest && cliLatest !== PKG_VERSION) {
852
- console.log(` LDM OS CLI v${PKG_VERSION} -> v${cliLatest} (auto-updates on install)`);
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/ dirs
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-install-* dirs
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "main": "src/boot/boot-hook.mjs",