@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 +1 -1
- package/SKILL.md +1 -1
- package/bin/ldm.js +108 -22
- package/docs/skills/README.md +1 -1
- package/docs/universal-installer/TECHNICAL.md +43 -0
- package/lib/deploy.mjs +9 -1
- package/package.json +1 -1
- package/docs/skills/TECHNICAL.md +0 -67
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](
|
|
106
|
+
- [Architecture, principles, and technical details](TECHNICAL.md)
|
|
107
107
|
|
|
108
108
|
## License
|
|
109
109
|
|
package/SKILL.md
CHANGED
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
627
|
-
|
|
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
|
-
|
|
637
|
-
|
|
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 ${
|
|
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}/${
|
|
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/' },
|
package/docs/skills/README.md
CHANGED
|
@@ -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
|
|
696
|
+
source,
|
|
689
697
|
interfaces: ifaceNames,
|
|
690
698
|
};
|
|
691
699
|
|
package/package.json
CHANGED
package/docs/skills/TECHNICAL.md
DELETED
|
@@ -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 |
|