@wipcomputer/wip-ldm-os 0.2.9 → 0.2.10

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 CHANGED
@@ -5,7 +5,7 @@ license: MIT
5
5
  interface: [cli, skill]
6
6
  metadata:
7
7
  display-name: "LDM OS"
8
- version: "0.2.9"
8
+ version: "0.2.10"
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
@@ -82,7 +82,6 @@ async function cmdInit() {
82
82
  join(LDM_ROOT, 'agents'),
83
83
  join(LDM_ROOT, 'memory'),
84
84
  join(LDM_ROOT, 'state'),
85
- join(LDM_ROOT, 'secrets'),
86
85
  join(LDM_ROOT, 'shared', 'boot'),
87
86
  ];
88
87
 
@@ -300,10 +299,37 @@ async function cmdInstall() {
300
299
  return;
301
300
  }
302
301
 
303
- // Resolve target: GitHub URL, org/repo shorthand, or local path
302
+ // Resolve target: npm package, GitHub URL, org/repo shorthand, or local path
304
303
  let repoPath;
305
304
 
306
- if (target.startsWith('http') || target.startsWith('git@') || target.match(/^[\w-]+\/[\w.-]+$/)) {
305
+ // Check if target looks like an npm package (starts with @ or is a plain name without /)
306
+ if (target.startsWith('@') || (!target.includes('/') && !existsSync(resolve(target)))) {
307
+ // Try npm install to temp dir
308
+ const npmName = target;
309
+ const tempDir = join('/tmp', `ldm-install-npm-${Date.now()}`);
310
+ console.log('');
311
+ console.log(` Installing ${npmName} from npm...`);
312
+ try {
313
+ mkdirSync(tempDir, { recursive: true });
314
+ execSync(`npm install ${npmName} --prefix "${tempDir}"`, { stdio: 'pipe' });
315
+ // Find the installed package in node_modules
316
+ const pkgName = npmName.startsWith('@') ? npmName : npmName;
317
+ const installed = join(tempDir, 'node_modules', pkgName);
318
+ if (existsSync(installed)) {
319
+ console.log(` + Installed from npm`);
320
+ repoPath = installed;
321
+ } else {
322
+ console.error(` x Package installed but not found at expected path`);
323
+ process.exit(1);
324
+ }
325
+ } catch (e) {
326
+ // npm failed, fall through to git clone or path resolution
327
+ console.log(` npm install failed, trying other methods...`);
328
+ try { execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' }); } catch {}
329
+ }
330
+ }
331
+
332
+ if (!repoPath && (target.startsWith('http') || target.startsWith('git@') || target.match(/^[\w-]+\/[\w.-]+$/))) {
307
333
  const isShorthand = target.match(/^[\w-]+\/[\w.-]+$/);
308
334
  const httpsUrl = isShorthand
309
335
  ? `https://github.com/${target}.git`
@@ -332,7 +358,7 @@ async function cmdInstall() {
332
358
  console.error(` x Clone failed: ${e.message}`);
333
359
  process.exit(1);
334
360
  }
335
- } else {
361
+ } else if (!repoPath) {
336
362
  repoPath = resolve(target);
337
363
  if (!existsSync(repoPath)) {
338
364
  console.error(` x Path not found: ${repoPath}`);
package/lib/deploy.mjs CHANGED
@@ -159,8 +159,34 @@ function runBuildIfNeeded(repoPath) {
159
159
  }
160
160
  }
161
161
 
162
+ // ── Version comparison (fix #7) ──
163
+
164
+ function compareSemver(a, b) {
165
+ if (!a || !b) return 0;
166
+ const pa = a.split('.').map(Number);
167
+ const pb = b.split('.').map(Number);
168
+ for (let i = 0; i < 3; i++) {
169
+ const na = pa[i] || 0;
170
+ const nb = pb[i] || 0;
171
+ if (na > nb) return 1;
172
+ if (na < nb) return -1;
173
+ }
174
+ return 0;
175
+ }
176
+
177
+ // Config files that should never be overwritten during updates
178
+ const PRESERVE_PATTERNS = [
179
+ 'boot-config.json', '.env', '.env.local',
180
+ 'config.local.json', 'settings.local.json',
181
+ ];
182
+
183
+ function isPreservedFile(filename) {
184
+ return PRESERVE_PATTERNS.some(p => filename === p || filename.endsWith('.local'));
185
+ }
186
+
162
187
  // ── Safe deploy (fix #7) ──
163
188
  // Deploy to temp dir first, then atomic rename. Never rm -rf the live dir.
189
+ // Preserves config files from existing installs.
164
190
 
165
191
  function copyFiltered(src, dest) {
166
192
  cpSync(src, dest, {
@@ -169,6 +195,23 @@ function copyFiltered(src, dest) {
169
195
  });
170
196
  }
171
197
 
198
+ function restorePreservedFiles(oldDir, newDir) {
199
+ if (!existsSync(oldDir)) return;
200
+ try {
201
+ const entries = readdirSync(oldDir);
202
+ for (const entry of entries) {
203
+ if (isPreservedFile(entry)) {
204
+ const oldPath = join(oldDir, entry);
205
+ const newPath = join(newDir, entry);
206
+ if (existsSync(oldPath) && !existsSync(newPath)) {
207
+ cpSync(oldPath, newPath);
208
+ ok(`Preserved config: ${entry}`);
209
+ }
210
+ }
211
+ }
212
+ } catch {}
213
+ }
214
+
172
215
  function safeDeployDir(repoPath, destDir, name) {
173
216
  const finalPath = join(destDir, name);
174
217
  const tempPath = join(tmpdir(), `ldm-deploy-${name}-${Date.now()}`);
@@ -200,7 +243,12 @@ function safeDeployDir(repoPath, destDir, name) {
200
243
  }
201
244
  renameSync(tempPath, finalPath);
202
245
 
203
- // 5. Trash the old version (never delete)
246
+ // 5. Restore preserved config files from old version
247
+ if (existsSync(backupPath)) {
248
+ restorePreservedFiles(backupPath, finalPath);
249
+ }
250
+
251
+ // 6. Trash the old version (never delete)
204
252
  if (existsSync(backupPath)) {
205
253
  const trashed = moveToTrash(backupPath);
206
254
  if (trashed) ok(`Old version moved to ${trashed}`);
@@ -337,8 +385,9 @@ function deployExtension(repoPath, name) {
337
385
  const newVersion = sourcePkg?.version;
338
386
  const currentVersion = installedPkg?.version;
339
387
 
340
- if (newVersion && currentVersion && newVersion === currentVersion) {
341
- skip(`LDM: ${name} already at v${currentVersion}`);
388
+ const cmp = compareSemver(newVersion, currentVersion);
389
+ if (newVersion && currentVersion && cmp <= 0) {
390
+ skip(`LDM: ${name} already at v${currentVersion}${cmp < 0 ? ` (source is older: v${newVersion})` : ''}`);
342
391
  // Ensure OC copy exists too
343
392
  const ocName = resolveOcPluginName(repoPath, name);
344
393
  const ocDest = join(OC_EXTENSIONS, ocName);
@@ -382,11 +431,35 @@ function deployExtension(repoPath, name) {
382
431
  fail(`OpenClaw: deploy failed for ${ocName}`);
383
432
  } else {
384
433
  ok(`OpenClaw: deployed to ${join(OC_EXTENSIONS, ocName)}`);
434
+ // Verify openclaw.json references match actual directory
435
+ verifyOcConfig(ocName);
385
436
  }
386
437
 
387
438
  return true;
388
439
  }
389
440
 
441
+ function verifyOcConfig(pluginDirName) {
442
+ const ocConfigPath = join(OC_ROOT, 'openclaw.json');
443
+ const ocConfig = readJSON(ocConfigPath);
444
+ if (!ocConfig?.extensions) return;
445
+
446
+ const pluginPath = join(OC_EXTENSIONS, pluginDirName);
447
+ const pluginJson = readJSON(join(pluginPath, 'openclaw.plugin.json'));
448
+ if (!pluginJson?.id) return;
449
+
450
+ // Check if openclaw.json has an entry whose path references a different dir
451
+ for (const ext of ocConfig.extensions) {
452
+ if (ext.id === pluginJson.id) {
453
+ const configDir = basename(ext.path || '');
454
+ if (configDir && configDir !== pluginDirName) {
455
+ log(`Warning: openclaw.json references "${configDir}" but plugin is at "${pluginDirName}"`);
456
+ log(` Update openclaw.json or rename the directory to match.`);
457
+ }
458
+ return;
459
+ }
460
+ }
461
+ }
462
+
390
463
  function registerMCP(repoPath, door, toolName) {
391
464
  const rawName = toolName || door.name || basename(repoPath);
392
465
  const name = rawName.replace(/^@[\w-]+\//, '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",