@wipcomputer/wip-ldm-os 0.4.2 → 0.4.3

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/README.md CHANGED
@@ -103,7 +103,7 @@ The OS connects your AIs. Add-ons are what they actually use. Each one is a full
103
103
 
104
104
  ## More Info
105
105
 
106
- - [Architecture, principles, and technical details](docs/universal-installer/TECHNICAL.md)
106
+ - [Architecture, principles, and technical details](TECHNICAL.md)
107
107
 
108
108
  ## License
109
109
 
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.2"
8
+ version: "0.4.3"
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
@@ -522,7 +522,7 @@ function autoDetectExtensions() {
522
522
  const dirs = readdirSync(LDM_EXTENSIONS, { withFileTypes: true });
523
523
  for (const dir of dirs) {
524
524
  if (!dir.isDirectory()) continue;
525
- if (dir.name === '_trash' || dir.name.startsWith('.')) continue;
525
+ if (dir.name === '_trash' || dir.name.startsWith('.') || dir.name.startsWith('ldm-install-')) continue;
526
526
 
527
527
  const extPath = join(LDM_EXTENSIONS, dir.name);
528
528
  const pkgPath = join(extPath, 'package.json');
@@ -603,14 +603,61 @@ async function cmdInstallCatalog() {
603
603
  console.log('');
604
604
  }
605
605
 
606
- if (DRY_RUN) {
607
- // Show what an update would do
608
- const updatable = Object.values(reconciled).filter(e =>
609
- e.registryHasSource
610
- );
606
+ // Build the update plan: extensions with valid source paths + catalog items with npm updates (#55)
607
+ const fromSource = Object.values(reconciled).filter(e => e.registryHasSource);
608
+
609
+ // For extensions without valid source: check catalog for repo, check npm for newer version
610
+ const fromCatalog = [];
611
+ for (const [name, entry] of Object.entries(reconciled)) {
612
+ if (entry.registryHasSource) continue; // already handled above
613
+ if (!entry.deployedLdm && !entry.deployedOc) continue; // not installed
614
+
615
+ // Find this extension in the catalog
616
+ const catalogEntry = components.find(c => {
617
+ const matches = c.registryMatches || [c.id];
618
+ return matches.includes(name) || c.id === name;
619
+ });
620
+ if (!catalogEntry?.repo) continue;
621
+
622
+ // Check npm for newer version
623
+ const npmPkg = catalogEntry.npm;
624
+ const currentVersion = entry.ldmVersion || entry.ocVersion;
625
+ let latestVersion = null;
626
+
627
+ if (npmPkg && currentVersion) {
628
+ try {
629
+ latestVersion = execSync(`npm view ${npmPkg} version 2>/dev/null`, {
630
+ encoding: 'utf8', timeout: 10000,
631
+ }).trim();
632
+ } catch {}
633
+ }
634
+
635
+ const hasUpdate = latestVersion && currentVersion && latestVersion !== currentVersion;
636
+ fromCatalog.push({
637
+ ...entry,
638
+ catalogRepo: catalogEntry.repo,
639
+ catalogNpm: npmPkg,
640
+ currentVersion,
641
+ latestVersion,
642
+ hasUpdate,
643
+ });
644
+ }
645
+
646
+ const updatable = fromSource;
647
+ const npmUpdates = fromCatalog.filter(e => e.hasUpdate);
648
+ const totalUpdates = updatable.length + npmUpdates.length;
611
649
 
650
+ if (DRY_RUN) {
612
651
  if (updatable.length > 0) {
613
652
  console.log(` Would update ${updatable.length} extension(s) from source repos.`);
653
+ }
654
+ if (npmUpdates.length > 0) {
655
+ console.log(` Would update ${npmUpdates.length} extension(s) from npm:`);
656
+ for (const e of npmUpdates) {
657
+ console.log(` ${e.name}: v${e.currentVersion} -> v${e.latestVersion} (${e.catalogNpm})`);
658
+ }
659
+ }
660
+ if (totalUpdates > 0) {
614
661
  console.log(' No data (crystal.db, agent files) would be touched.');
615
662
  console.log(' Old versions would be moved to ~/.ldm/_trash/ (never deleted).');
616
663
  } else {
@@ -623,18 +670,15 @@ async function cmdInstallCatalog() {
623
670
  return;
624
671
  }
625
672
 
626
- // Real update: only touch things with linked source repos
627
- const updatable = Object.values(reconciled).filter(e =>
628
- e.registryHasSource
629
- );
630
-
631
- if (updatable.length === 0) {
632
- console.log(' No extensions have linked source repos to update from.');
633
- console.log(' Link them with: ldm install <org/repo>');
673
+ if (totalUpdates === 0 && available.length === 0) {
674
+ console.log(' Everything is up to date.');
634
675
  console.log('');
676
+ return;
677
+ }
635
678
 
636
- // Still offer catalog install if TTY
637
- if (available.length > 0 && !YES_FLAG && !NONE_FLAG && process.stdin.isTTY) {
679
+ if (totalUpdates === 0 && available.length > 0) {
680
+ // Nothing to update, but catalog items available
681
+ if (!YES_FLAG && !NONE_FLAG && process.stdin.isTTY) {
638
682
  const { createInterface } = await import('node:readline');
639
683
  const rl = createInterface({ input: process.stdin, output: process.stdout });
640
684
  const answer = await new Promise((resolve) => {
@@ -643,7 +687,6 @@ async function cmdInstallCatalog() {
643
687
  resolve(a.trim().toLowerCase());
644
688
  });
645
689
  });
646
-
647
690
  if (answer && answer !== 'none' && answer !== 'n') {
648
691
  let toInstall = [];
649
692
  if (answer === 'all' || answer === 'a') {
@@ -668,13 +711,19 @@ async function cmdInstallCatalog() {
668
711
  // Write revert manifest before starting
669
712
  const { createRevertManifest } = await import('../lib/safe.mjs');
670
713
  const manifestPath = createRevertManifest(
671
- `ldm install (update ${updatable.length} extensions)`,
672
- updatable.map(e => ({
673
- action: 'update',
714
+ `ldm install (update ${totalUpdates} extensions)`,
715
+ [...updatable.map(e => ({
716
+ action: 'update-from-source',
674
717
  name: e.name,
675
718
  currentVersion: e.ldmVersion || e.registryVersion,
676
719
  source: e.registrySource,
677
- }))
720
+ })), ...npmUpdates.map(e => ({
721
+ action: 'update-from-catalog',
722
+ name: e.name,
723
+ currentVersion: e.currentVersion,
724
+ latestVersion: e.latestVersion,
725
+ repo: e.catalogRepo,
726
+ }))]
678
727
  );
679
728
  console.log(` Revert plan saved: ${manifestPath}`);
680
729
  console.log('');
@@ -683,18 +732,31 @@ async function cmdInstallCatalog() {
683
732
  setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT });
684
733
 
685
734
  let updated = 0;
735
+
736
+ // Update from source repos (local paths)
686
737
  for (const entry of updatable) {
687
738
  await installFromPath(entry.registrySource);
688
739
  updated++;
689
740
  }
690
741
 
742
+ // Update from catalog repos (clone from GitHub for extensions without valid source) (#55)
743
+ for (const entry of npmUpdates) {
744
+ console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from ${entry.catalogRepo})...`);
745
+ try {
746
+ execSync(`ldm install ${entry.catalogRepo}`, { stdio: 'inherit' });
747
+ updated++;
748
+ } catch (e) {
749
+ console.error(` x Failed to update ${entry.name}: ${e.message}`);
750
+ }
751
+ }
752
+
691
753
  // Sync boot hook from npm package (#49)
692
754
  if (syncBootHook()) {
693
755
  ok('Boot hook updated (sessions, messages, updates now active)');
694
756
  }
695
757
 
696
758
  console.log('');
697
- console.log(` Updated ${updated}/${updatable.length} extension(s).`);
759
+ console.log(` Updated ${updated}/${totalUpdates} extension(s).`);
698
760
 
699
761
  // Check if CLI itself is outdated (#29)
700
762
  checkCliVersion();
@@ -780,6 +842,30 @@ async function cmdDoctor() {
780
842
  }
781
843
  }
782
844
 
845
+ // --fix: clean registry entries with /tmp/ sources or ldm-install- names (#54)
846
+ if (FIX_FLAG) {
847
+ const registry = readJSON(REGISTRY_PATH);
848
+ if (registry?.extensions) {
849
+ const staleNames = [];
850
+ for (const [name, info] of Object.entries(registry.extensions)) {
851
+ const src = info?.source || '';
852
+ const isTmpSource = src.startsWith('/tmp/') || src.startsWith('/private/tmp/');
853
+ const isTmpName = name.startsWith('ldm-install-');
854
+ if (isTmpSource || isTmpName) {
855
+ staleNames.push(name);
856
+ }
857
+ }
858
+ for (const name of staleNames) {
859
+ delete registry.extensions[name];
860
+ console.log(` + Removed stale registry entry: ${name} (/tmp/ clone)`);
861
+ issues = Math.max(0, issues - 1);
862
+ }
863
+ if (staleNames.length > 0) {
864
+ writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
865
+ }
866
+ }
867
+ }
868
+
783
869
  // 4. Check sacred locations
784
870
  const sacred = [
785
871
  { path: join(LDM_ROOT, 'memory'), label: 'memory/' },
@@ -79,4 +79,4 @@ Your AIs are only as powerful as what you give them. Here's everything available
79
79
 
80
80
  ---
81
81
 
82
- [Technical Reference](./TECHNICAL.md)
82
+ [Technical Reference](../universal-installer/TECHNICAL.md)
@@ -243,6 +243,49 @@ The `ai/` folder is the development process. It is not part of the published pro
243
243
 
244
244
  **Public/private split:** If a repo is public, the `ai/` folder should not ship. The recommended pattern is to maintain a private working repo (with `ai/`) and a public repo (everything except `ai/`). The public repo has everything an LLM or human needs to understand and use the tool. The `ai/` folder is operational context for the team building it.
245
245
 
246
+ ## Catalog
247
+
248
+ Skills are defined in `catalog.json` at the LDM OS root. Each entry has:
249
+
250
+ ```json
251
+ {
252
+ "id": "memory-crystal",
253
+ "name": "Memory Crystal",
254
+ "description": "Persistent memory for your AI.",
255
+ "npm": "@wipcomputer/memory-crystal",
256
+ "repo": "wipcomputer/memory-crystal",
257
+ "registryMatches": ["memory-crystal"],
258
+ "cliMatches": ["crystal"],
259
+ "recommended": true,
260
+ "status": "stable"
261
+ }
262
+ ```
263
+
264
+ ## Stacks
265
+
266
+ Stacks group skills for team installs. Defined in `catalog.json`:
267
+
268
+ ```json
269
+ {
270
+ "stacks": {
271
+ "core": {
272
+ "name": "WIP Core",
273
+ "components": ["memory-crystal", "wip-ai-devops-toolbox", "wip-1password", "wip-markdown-viewer"],
274
+ "mcpServers": []
275
+ },
276
+ "web": {
277
+ "name": "Web Development",
278
+ "components": [],
279
+ "mcpServers": [
280
+ { "name": "playwright", "command": "npx", "args": ["-y", "@playwright/mcp@latest"] }
281
+ ]
282
+ }
283
+ }
284
+ }
285
+ ```
286
+
287
+ Stacks are composable via the `includes` field.
288
+
246
289
  ## The Installer
247
290
 
248
291
  `ldm install` scans any repo, detects which interfaces exist, and installs them all. One command.
package/lib/deploy.mjs CHANGED
@@ -682,10 +682,18 @@ export function installSingleTool(toolPath) {
682
682
  }
683
683
 
684
684
  let installed = 0;
685
+ // Don't store /tmp/ clone paths as source (#54). Use the repo URL from package.json if available.
686
+ let source = toolPath;
687
+ const isTmpPath = toolPath.startsWith('/tmp/') || toolPath.startsWith('/private/tmp/');
688
+ if (isTmpPath && pkg?.repository?.url) {
689
+ source = pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
690
+ } else if (isTmpPath) {
691
+ source = null; // better than a /tmp/ path
692
+ }
685
693
  const registryInfo = {
686
694
  name: toolName,
687
695
  version: pkg?.version || 'unknown',
688
- source: toolPath,
696
+ source,
689
697
  interfaces: ifaceNames,
690
698
  };
691
699
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",
@@ -1,67 +0,0 @@
1
- # Skills ... Technical Reference
2
-
3
- ## Catalog
4
-
5
- Skills are defined in `catalog.json` at the LDM OS root. Each entry has:
6
-
7
- ```json
8
- {
9
- "id": "memory-crystal",
10
- "name": "Memory Crystal",
11
- "description": "Persistent memory for your AI.",
12
- "npm": "@wipcomputer/memory-crystal",
13
- "repo": "wipcomputer/memory-crystal",
14
- "registryMatches": ["memory-crystal"],
15
- "cliMatches": ["crystal"],
16
- "recommended": true,
17
- "status": "stable"
18
- }
19
- ```
20
-
21
- ## Stacks
22
-
23
- Stacks group skills for team installs. Defined in `catalog.json`:
24
-
25
- ```json
26
- {
27
- "stacks": {
28
- "core": {
29
- "name": "WIP Core",
30
- "components": ["memory-crystal", "wip-ai-devops-toolbox", "wip-1password", "wip-markdown-viewer"],
31
- "mcpServers": []
32
- },
33
- "web": {
34
- "name": "Web Development",
35
- "components": [],
36
- "mcpServers": [
37
- { "name": "playwright", "command": "npx", "args": ["-y", "@playwright/mcp@latest"] }
38
- ]
39
- }
40
- }
41
- }
42
- ```
43
-
44
- Stacks are composable via the `includes` field.
45
-
46
- ## Interface Detection
47
-
48
- `ldm install` auto-detects which interfaces a repo supports:
49
-
50
- | Pattern | Interface | Install Action |
51
- |---------|-----------|---------------|
52
- | `package.json` with `bin` | CLI | `npm install -g` from registry |
53
- | `main` or `exports` | Module | Reports import path |
54
- | `mcp-server.mjs` | MCP | `claude mcp add --scope user` |
55
- | `openclaw.plugin.json` | OpenClaw Plugin | Deploy to `~/.ldm/extensions/` + `~/.openclaw/extensions/` |
56
- | `SKILL.md` | Skill | Deploy to `~/.openclaw/skills/` |
57
- | `guard.mjs` or `claudeCode.hook` | CC Hook | Add to `~/.claude/settings.json` |
58
- | `.claude-plugin/plugin.json` | CC Plugin | Register with marketplace |
59
-
60
- ## Key Files
61
-
62
- | File | What |
63
- |------|------|
64
- | `catalog.json` | Component + stack definitions |
65
- | `lib/detect.mjs` | Interface detection engine |
66
- | `lib/deploy.mjs` | Deployment engine |
67
- | `bin/ldm.js` | `ldm stack` and `ldm install` commands |