@wipcomputer/wip-ldm-os 0.4.29 → 0.4.30

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 +278 -21
  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.30"
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
  }
@@ -918,13 +949,21 @@ async function cmdInstallCatalog() {
918
949
  }
919
950
  } catch {}
920
951
 
921
- // Check orphaned /tmp/ dirs
952
+ // Check orphaned staging dirs (old /tmp/ and new ~/.ldm/tmp/)
922
953
  try {
923
954
  const tmpCount = readdirSync('/private/tmp').filter(d => d.startsWith('ldm-install-')).length;
924
955
  if (tmpCount > 0) {
925
956
  healthIssues.push(` ! ${tmpCount} orphaned /tmp/ldm-install-* dirs (would clean up)`);
926
957
  }
927
958
  } catch {}
959
+ try {
960
+ if (existsSync(LDM_TMP)) {
961
+ const ldmTmpCount = readdirSync(LDM_TMP).filter(d => d.startsWith('ldm-install-')).length;
962
+ if (ldmTmpCount > 0) {
963
+ healthIssues.push(` ! ${ldmTmpCount} orphaned ~/.ldm/tmp/ldm-install-* dirs (would clean up)`);
964
+ }
965
+ }
966
+ } catch {}
928
967
 
929
968
  if (healthIssues.length > 0) {
930
969
  console.log('');
@@ -1075,7 +1114,7 @@ async function cmdInstallCatalog() {
1075
1114
  }
1076
1115
  } catch {}
1077
1116
 
1078
- // 3. Clean orphaned /tmp/ldm-install-* dirs
1117
+ // 3. Clean orphaned staging dirs (old /tmp/ and new ~/.ldm/tmp/)
1079
1118
  try {
1080
1119
  const tmpDirs = readdirSync('/private/tmp').filter(d => d.startsWith('ldm-install-'));
1081
1120
  if (tmpDirs.length > 0) {
@@ -1087,6 +1126,19 @@ async function cmdInstallCatalog() {
1087
1126
  console.log(` + Cleaned ${tmpDirs.length} orphaned /tmp/ clone(s)`);
1088
1127
  }
1089
1128
  } catch {}
1129
+ try {
1130
+ if (existsSync(LDM_TMP)) {
1131
+ const ldmTmpDirs = readdirSync(LDM_TMP).filter(d => d.startsWith('ldm-install-'));
1132
+ if (ldmTmpDirs.length > 0) {
1133
+ console.log(` Cleaning ${ldmTmpDirs.length} orphaned ~/.ldm/tmp/ dirs...`);
1134
+ for (const d of ldmTmpDirs) {
1135
+ try { execSync(`rm -rf "${join(LDM_TMP, d)}"`, { stdio: 'pipe', timeout: 10000 }); } catch {}
1136
+ }
1137
+ healthFixes++;
1138
+ console.log(` + Cleaned ${ldmTmpDirs.length} orphaned ~/.ldm/tmp/ clone(s)`);
1139
+ }
1140
+ }
1141
+ } catch {}
1090
1142
 
1091
1143
  if (healthFixes > 0) {
1092
1144
  console.log(` ${healthFixes} health issue(s) fixed.`);
@@ -2036,6 +2088,208 @@ async function main() {
2036
2088
  console.log('');
2037
2089
  }
2038
2090
 
2091
+ // ── ldm uninstall (#114) ──
2092
+
2093
+ async function cmdUninstall() {
2094
+ const keepData = !args.includes('--all');
2095
+ const isDryRun = args.includes('--dry-run');
2096
+
2097
+ console.log('');
2098
+ console.log(' LDM OS Uninstall');
2099
+ console.log(' ────────────────────────────────────');
2100
+
2101
+ if (keepData) {
2102
+ console.log(' Your data will be PRESERVED:');
2103
+ console.log(' ~/.ldm/memory/ (crystal.db, shared memory)');
2104
+ console.log(' ~/.ldm/agents/ (identity, journals, daily logs)');
2105
+ console.log('');
2106
+ console.log(' Use --all to remove everything including data.');
2107
+ } else {
2108
+ console.log(' WARNING: --all flag set. ALL data will be removed.');
2109
+ console.log(' This includes crystal.db, agent files, journals, everything.');
2110
+ }
2111
+
2112
+ console.log('');
2113
+ console.log(' Will remove:');
2114
+
2115
+ // 1. MCP servers
2116
+ const claudeJsonPath = join(HOME, '.claude.json');
2117
+ try {
2118
+ const claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
2119
+ const mcpNames = Object.keys(claudeJson.mcpServers || {}).filter(n =>
2120
+ n.includes('crystal') || n.includes('wip-') || n.includes('memory') ||
2121
+ n.includes('grok') || n.includes('lesa') || n.includes('1password')
2122
+ );
2123
+ if (mcpNames.length > 0) {
2124
+ console.log(` MCP servers: ${mcpNames.join(', ')}`);
2125
+ }
2126
+ } catch {}
2127
+
2128
+ // 2. CC hooks
2129
+ const settingsPath = join(HOME, '.claude', 'settings.json');
2130
+ try {
2131
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
2132
+ let hookCount = 0;
2133
+ for (const [event, entries] of Object.entries(settings.hooks || {})) {
2134
+ for (const entry of (Array.isArray(entries) ? entries : [])) {
2135
+ for (const h of (entry.hooks || [])) {
2136
+ if (h.command?.includes('.ldm') || h.command?.includes('wip-')) hookCount++;
2137
+ }
2138
+ }
2139
+ }
2140
+ if (hookCount > 0) console.log(` CC hooks: ${hookCount} hook(s)`);
2141
+ } catch {}
2142
+
2143
+ // 3. Skills
2144
+ const skillsDir = join(HOME, '.openclaw', 'skills');
2145
+ try {
2146
+ const skills = readdirSync(skillsDir).filter(d => d !== '.DS_Store');
2147
+ if (skills.length > 0) console.log(` Skills: ${skills.join(', ')}`);
2148
+ } catch {}
2149
+
2150
+ // 4. Cron jobs
2151
+ try {
2152
+ const crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
2153
+ const ldmLines = crontab.split('\n').filter(l => l.includes('.ldm') || l.includes('crystal-capture') || l.includes('process-monitor'));
2154
+ if (ldmLines.length > 0) console.log(` Cron jobs: ${ldmLines.length}`);
2155
+ } catch {}
2156
+
2157
+ // 5. Global npm packages
2158
+ try {
2159
+ const npmList = execSync('npm list -g --depth=0 --json 2>/dev/null', { encoding: 'utf8' });
2160
+ const deps = JSON.parse(npmList).dependencies || {};
2161
+ const wipPkgs = Object.keys(deps).filter(n => n.startsWith('@wipcomputer/'));
2162
+ if (wipPkgs.length > 0) console.log(` npm packages: ${wipPkgs.join(', ')}`);
2163
+ } catch {}
2164
+
2165
+ // 6. Directories
2166
+ console.log(` ~/.ldm/extensions/`);
2167
+ if (!keepData) {
2168
+ console.log(` ~/.ldm/memory/`);
2169
+ console.log(` ~/.ldm/agents/`);
2170
+ }
2171
+ console.log(` ~/.ldm/state/, bin/, hooks/, logs/, sessions/, messages/`);
2172
+
2173
+ if (isDryRun) {
2174
+ console.log('');
2175
+ console.log(' Dry run. Nothing removed.');
2176
+ console.log('');
2177
+ return;
2178
+ }
2179
+
2180
+ // Confirm
2181
+ if (process.stdin.isTTY) {
2182
+ const { createInterface } = await import('readline');
2183
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2184
+ const answer = await new Promise(resolve => {
2185
+ rl.question('\n Type "uninstall" to confirm: ', resolve);
2186
+ });
2187
+ rl.close();
2188
+ if (answer.trim() !== 'uninstall') {
2189
+ console.log(' Cancelled.');
2190
+ return;
2191
+ }
2192
+ }
2193
+
2194
+ console.log('');
2195
+ console.log(' Removing...');
2196
+
2197
+ // 1. Unregister MCP servers
2198
+ try {
2199
+ const claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
2200
+ const mcpNames = Object.keys(claudeJson.mcpServers || {}).filter(n =>
2201
+ n.includes('crystal') || n.includes('wip-') || n.includes('memory') ||
2202
+ n.includes('grok') || n.includes('lesa') || n.includes('1password')
2203
+ );
2204
+ for (const name of mcpNames) {
2205
+ delete claudeJson.mcpServers[name];
2206
+ }
2207
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + '\n');
2208
+ console.log(` + Removed ${mcpNames.length} MCP server(s)`);
2209
+ } catch {}
2210
+
2211
+ // 2. Remove CC hooks
2212
+ try {
2213
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
2214
+ let removed = 0;
2215
+ for (const [event, entries] of Object.entries(settings.hooks || {})) {
2216
+ if (!Array.isArray(entries)) continue;
2217
+ for (const entry of entries) {
2218
+ if (!entry.hooks) continue;
2219
+ const before = entry.hooks.length;
2220
+ entry.hooks = entry.hooks.filter(h => !h.command?.includes('.ldm') && !h.command?.includes('wip-'));
2221
+ removed += before - entry.hooks.length;
2222
+ }
2223
+ settings.hooks[event] = entries.filter(e => e.hooks?.length > 0);
2224
+ }
2225
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
2226
+ console.log(` + Removed ${removed} CC hook(s)`);
2227
+ } catch {}
2228
+
2229
+ // 3. Remove skills
2230
+ try {
2231
+ const skills = readdirSync(skillsDir).filter(d => d !== '.DS_Store');
2232
+ for (const s of skills) {
2233
+ execSync(`rm -rf "${join(skillsDir, s)}"`, { stdio: 'pipe' });
2234
+ }
2235
+ console.log(` + Removed ${skills.length} skill(s)`);
2236
+ } catch {}
2237
+
2238
+ // 4. Remove cron jobs
2239
+ try {
2240
+ const crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
2241
+ const filtered = crontab.split('\n').filter(l =>
2242
+ !l.includes('.ldm') && !l.includes('crystal-capture') && !l.includes('process-monitor')
2243
+ ).join('\n');
2244
+ execSync(`echo "${filtered}" | crontab -`, { stdio: 'pipe' });
2245
+ console.log(' + Cleaned cron jobs');
2246
+ } catch {}
2247
+
2248
+ // 5. Remove npm packages
2249
+ try {
2250
+ const npmList = execSync('npm list -g --depth=0 --json 2>/dev/null', { encoding: 'utf8' });
2251
+ const deps = JSON.parse(npmList).dependencies || {};
2252
+ const wipPkgs = Object.keys(deps).filter(n => n.startsWith('@wipcomputer/'));
2253
+ for (const pkg of wipPkgs) {
2254
+ if (pkg === '@wipcomputer/wip-ldm-os') continue; // uninstall self last
2255
+ try { execSync(`npm uninstall -g ${pkg}`, { stdio: 'pipe', timeout: 30000 }); } catch {}
2256
+ }
2257
+ console.log(` + Removed ${wipPkgs.length - 1} npm package(s)`);
2258
+ } catch {}
2259
+
2260
+ // 6. Remove directories
2261
+ const dirsToRemove = ['extensions', 'state', 'bin', 'hooks', 'logs', 'sessions', 'messages', 'shared', '_trash'];
2262
+ if (!keepData) {
2263
+ dirsToRemove.push('memory', 'agents', 'secrets', 'backups');
2264
+ }
2265
+ for (const dir of dirsToRemove) {
2266
+ const p = join(LDM_ROOT, dir);
2267
+ if (existsSync(p)) {
2268
+ execSync(`rm -rf "${p}"`, { stdio: 'pipe' });
2269
+ }
2270
+ }
2271
+ // Remove config and version files
2272
+ for (const f of ['version.json', 'config.json']) {
2273
+ const p = join(LDM_ROOT, f);
2274
+ if (existsSync(p)) unlinkSync(p);
2275
+ }
2276
+ console.log(' + Removed ~/.ldm/ contents');
2277
+
2278
+ if (keepData) {
2279
+ console.log('');
2280
+ console.log(' Preserved:');
2281
+ console.log(' ~/.ldm/memory/ (your data)');
2282
+ console.log(' ~/.ldm/agents/ (identity + journals)');
2283
+ }
2284
+
2285
+ // 7. Self-uninstall
2286
+ console.log('');
2287
+ console.log(' To finish, run: npm uninstall -g @wipcomputer/wip-ldm-os');
2288
+ console.log('');
2289
+ console.log(' LDM OS uninstalled.');
2290
+ console.log('');
2291
+ }
2292
+
2039
2293
  if (command === '--version' || command === '-v') {
2040
2294
  console.log(PKG_VERSION);
2041
2295
  process.exit(0);
@@ -2079,6 +2333,9 @@ async function main() {
2079
2333
  case 'disable':
2080
2334
  await cmdDisable();
2081
2335
  break;
2336
+ case 'uninstall':
2337
+ await cmdUninstall();
2338
+ break;
2082
2339
  default:
2083
2340
  console.error(` Unknown command: ${command}`);
2084
2341
  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.30",
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",