@wipcomputer/wip-ldm-os 0.2.1

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/bin/ldm.mjs ADDED
@@ -0,0 +1,671 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ldm - LDM OS CLI
4
+ * The kernel for agent-native software.
5
+ *
6
+ * Commands:
7
+ * ldm init Scaffold ~/.ldm/ and write version.json
8
+ * ldm install <target> Detect interfaces + deploy + register
9
+ * ldm install Install/update all registered components
10
+ * ldm doctor Check health of all extensions
11
+ * ldm status Show LDM OS version and extension count
12
+ * ldm --version Show version
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
16
+ import { join, basename, resolve, dirname } from 'node:path';
17
+ import { execSync } from 'node:child_process';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ const HOME = process.env.HOME || '';
24
+ const LDM_ROOT = join(HOME, '.ldm');
25
+ const LDM_EXTENSIONS = join(LDM_ROOT, 'extensions');
26
+ const VERSION_PATH = join(LDM_ROOT, 'version.json');
27
+ const REGISTRY_PATH = join(LDM_EXTENSIONS, 'registry.json');
28
+
29
+ // Read our own version from package.json
30
+ const pkgPath = join(__dirname, '..', 'package.json');
31
+ let PKG_VERSION = '0.2.0';
32
+ try {
33
+ PKG_VERSION = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
34
+ } catch {}
35
+
36
+ // Read catalog
37
+ const catalogPath = join(__dirname, '..', 'catalog.json');
38
+ let CATALOG = { components: [] };
39
+ try {
40
+ CATALOG = JSON.parse(readFileSync(catalogPath, 'utf8'));
41
+ } catch {}
42
+
43
+ const args = process.argv.slice(2);
44
+ const command = args[0];
45
+ const DRY_RUN = args.includes('--dry-run');
46
+ const JSON_OUTPUT = args.includes('--json');
47
+ const YES_FLAG = args.includes('--yes') || args.includes('-y');
48
+ const NONE_FLAG = args.includes('--none');
49
+
50
+ function readJSON(path) {
51
+ try {
52
+ return JSON.parse(readFileSync(path, 'utf8'));
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function writeJSON(path, data) {
59
+ mkdirSync(dirname(path), { recursive: true });
60
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
61
+ }
62
+
63
+ // ── Catalog helpers ──
64
+
65
+ function loadCatalog() {
66
+ return CATALOG.components || [];
67
+ }
68
+
69
+ function findInCatalog(id) {
70
+ return loadCatalog().find(c => c.id === id);
71
+ }
72
+
73
+ // ── ldm init ──
74
+
75
+ async function cmdInit() {
76
+ console.log('');
77
+ console.log(' ldm init');
78
+ console.log(' ────────────────────────────────────');
79
+
80
+ const dirs = [
81
+ join(LDM_ROOT, 'extensions'),
82
+ join(LDM_ROOT, 'agents'),
83
+ join(LDM_ROOT, 'memory'),
84
+ join(LDM_ROOT, 'state'),
85
+ join(LDM_ROOT, 'secrets'),
86
+ join(LDM_ROOT, 'shared', 'boot'),
87
+ ];
88
+
89
+ const existing = existsSync(VERSION_PATH);
90
+
91
+ if (DRY_RUN) {
92
+ for (const dir of dirs) {
93
+ if (existsSync(dir)) {
94
+ console.log(` - ${dir} (exists)`);
95
+ } else {
96
+ console.log(` + would create ${dir}`);
97
+ }
98
+ }
99
+ if (existing) {
100
+ const v = readJSON(VERSION_PATH);
101
+ console.log(` - version.json exists (v${v?.version})`);
102
+ } else {
103
+ console.log(` + would write version.json (v${PKG_VERSION})`);
104
+ }
105
+ console.log('');
106
+ console.log(' Dry run complete. No changes made.');
107
+ console.log('');
108
+ return;
109
+ }
110
+
111
+ let created = 0;
112
+ for (const dir of dirs) {
113
+ if (!existsSync(dir)) {
114
+ mkdirSync(dir, { recursive: true });
115
+ console.log(` + ${dir}`);
116
+ created++;
117
+ } else {
118
+ console.log(` - ${dir} (exists)`);
119
+ }
120
+ }
121
+
122
+ // Write or update version.json
123
+ const now = new Date().toISOString();
124
+ if (existing) {
125
+ const v = readJSON(VERSION_PATH);
126
+ v.version = PKG_VERSION;
127
+ v.updated = now;
128
+ writeJSON(VERSION_PATH, v);
129
+ console.log(` + version.json updated to v${PKG_VERSION}`);
130
+ } else {
131
+ writeJSON(VERSION_PATH, {
132
+ version: PKG_VERSION,
133
+ installed: now,
134
+ updated: now,
135
+ });
136
+ console.log(` + version.json created (v${PKG_VERSION})`);
137
+ }
138
+
139
+ // Seed registry if missing
140
+ if (!existsSync(REGISTRY_PATH)) {
141
+ writeJSON(REGISTRY_PATH, { _format: 'v1', extensions: {} });
142
+ console.log(` + registry.json created`);
143
+ }
144
+
145
+ console.log('');
146
+ console.log(` LDM OS v${PKG_VERSION} initialized at ${LDM_ROOT}`);
147
+ console.log('');
148
+
149
+ // Show catalog picker (unless --none or --dry-run)
150
+ if (!NONE_FLAG && !DRY_RUN) {
151
+ await showCatalogPicker();
152
+ }
153
+ }
154
+
155
+ async function showCatalogPicker() {
156
+ const components = loadCatalog();
157
+ if (components.length === 0) return;
158
+
159
+ // Check what's already installed
160
+ const registry = readJSON(REGISTRY_PATH);
161
+ const installed = Object.keys(registry?.extensions || {});
162
+
163
+ const available = components.filter(c => c.status !== 'coming-soon' && !installed.includes(c.id));
164
+ const comingSoon = components.filter(c => c.status === 'coming-soon');
165
+
166
+ if (available.length === 0 && comingSoon.length === 0) return;
167
+
168
+ console.log(' Available components:');
169
+ console.log('');
170
+
171
+ let idx = 1;
172
+ const selectable = [];
173
+ for (const c of available) {
174
+ const rec = c.recommended ? ' (recommended)' : '';
175
+ console.log(` ${idx}. ${c.name}${rec}`);
176
+ console.log(` ${c.description}`);
177
+ console.log('');
178
+ selectable.push(c);
179
+ idx++;
180
+ }
181
+
182
+ for (const c of comingSoon) {
183
+ console.log(` ${idx}. ${c.name} (coming soon)`);
184
+ console.log(` ${c.description}`);
185
+ console.log('');
186
+ idx++;
187
+ }
188
+
189
+ // If --yes, install recommended only
190
+ if (YES_FLAG) {
191
+ const recommended = selectable.filter(c => c.recommended);
192
+ if (recommended.length > 0) {
193
+ for (const c of recommended) {
194
+ console.log(` Installing ${c.name}...`);
195
+ try {
196
+ execSync(`ldm install ${c.repo}`, { stdio: 'inherit' });
197
+ } catch (e) {
198
+ console.error(` x Failed to install ${c.name}: ${e.message}`);
199
+ }
200
+ }
201
+ }
202
+ return;
203
+ }
204
+
205
+ // Interactive prompt (skip if not a TTY)
206
+ if (!process.stdin.isTTY) return;
207
+
208
+ const { createInterface } = await import('node:readline');
209
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
210
+
211
+ const answer = await new Promise((resolve) => {
212
+ rl.question(' Install components? [1,2,all,none]: ', (a) => {
213
+ rl.close();
214
+ resolve(a.trim().toLowerCase());
215
+ });
216
+ });
217
+
218
+ if (!answer || answer === 'none' || answer === 'n') return;
219
+
220
+ let toInstall = [];
221
+ if (answer === 'all' || answer === 'a') {
222
+ toInstall = selectable;
223
+ } else {
224
+ const nums = answer.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
225
+ toInstall = nums.map(n => selectable[n - 1]).filter(Boolean);
226
+ }
227
+
228
+ for (const c of toInstall) {
229
+ console.log('');
230
+ console.log(` Installing ${c.name}...`);
231
+ try {
232
+ execSync(`ldm install ${c.repo}`, { stdio: 'inherit' });
233
+ } catch (e) {
234
+ console.error(` x Failed to install ${c.name}: ${e.message}`);
235
+ }
236
+ }
237
+ }
238
+
239
+ // ── ldm install ──
240
+
241
+ async function cmdInstall() {
242
+ // Ensure LDM is initialized
243
+ if (!existsSync(VERSION_PATH)) {
244
+ console.log(' LDM OS not initialized. Running init first...');
245
+ console.log('');
246
+ cmdInit();
247
+ }
248
+
249
+ const { setFlags, installFromPath, installSingleTool, installToolbox } = await import('../lib/deploy.mjs');
250
+ const { detectInterfacesJSON } = await import('../lib/detect.mjs');
251
+
252
+ setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT });
253
+
254
+ // Find the target (skip flags)
255
+ const target = args.slice(1).find(a => !a.startsWith('--'));
256
+
257
+ if (!target) {
258
+ // Bare `ldm install`: show catalog status + update registered
259
+ return cmdInstallCatalog();
260
+ }
261
+
262
+ // Check if target is a catalog ID (e.g. "memory-crystal")
263
+ const catalogEntry = findInCatalog(target);
264
+ if (catalogEntry) {
265
+ console.log('');
266
+ console.log(` Resolved "${target}" via catalog to ${catalogEntry.repo}`);
267
+
268
+ // Use the repo field to clone from GitHub
269
+ const repoTarget = catalogEntry.repo;
270
+ const repoName = basename(repoTarget);
271
+ const repoPath = join('/tmp', `ldm-install-${repoName}`);
272
+ const httpsUrl = `https://github.com/${repoTarget}.git`;
273
+ const sshUrl = `git@github.com:${repoTarget}.git`;
274
+
275
+ console.log(` Cloning ${repoTarget}...`);
276
+ try {
277
+ if (existsSync(repoPath)) {
278
+ execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
279
+ }
280
+ try {
281
+ execSync(`git clone "${httpsUrl}" "${repoPath}"`, { stdio: 'pipe' });
282
+ } catch {
283
+ console.log(` HTTPS failed. Trying SSH...`);
284
+ if (existsSync(repoPath)) execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
285
+ execSync(`git clone "${sshUrl}" "${repoPath}"`, { stdio: 'pipe' });
286
+ }
287
+ console.log(` + Cloned to ${repoPath}`);
288
+ } catch (e) {
289
+ console.error(` x Clone failed: ${e.message}`);
290
+ process.exit(1);
291
+ }
292
+
293
+ if (JSON_OUTPUT) {
294
+ const result = detectInterfacesJSON(repoPath);
295
+ console.log(JSON.stringify(result, null, 2));
296
+ if (DRY_RUN) process.exit(0);
297
+ }
298
+
299
+ await installFromPath(repoPath);
300
+ return;
301
+ }
302
+
303
+ // Resolve target: GitHub URL, org/repo shorthand, or local path
304
+ let repoPath;
305
+
306
+ if (target.startsWith('http') || target.startsWith('git@') || target.match(/^[\w-]+\/[\w.-]+$/)) {
307
+ const isShorthand = target.match(/^[\w-]+\/[\w.-]+$/);
308
+ const httpsUrl = isShorthand
309
+ ? `https://github.com/${target}.git`
310
+ : target;
311
+ const sshUrl = isShorthand
312
+ ? `git@github.com:${target}.git`
313
+ : target.replace(/^https:\/\/github\.com\//, 'git@github.com:');
314
+ const repoName = basename(httpsUrl).replace('.git', '');
315
+ repoPath = join('/tmp', `ldm-install-${repoName}`);
316
+
317
+ console.log('');
318
+ console.log(` Cloning ${isShorthand ? target : httpsUrl}...`);
319
+ try {
320
+ if (existsSync(repoPath)) {
321
+ execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
322
+ }
323
+ try {
324
+ execSync(`git clone "${httpsUrl}" "${repoPath}"`, { stdio: 'pipe' });
325
+ } catch {
326
+ console.log(` HTTPS failed. Trying SSH...`);
327
+ if (existsSync(repoPath)) execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
328
+ execSync(`git clone "${sshUrl}" "${repoPath}"`, { stdio: 'pipe' });
329
+ }
330
+ console.log(` + Cloned to ${repoPath}`);
331
+ } catch (e) {
332
+ console.error(` x Clone failed: ${e.message}`);
333
+ process.exit(1);
334
+ }
335
+ } else {
336
+ repoPath = resolve(target);
337
+ if (!existsSync(repoPath)) {
338
+ console.error(` x Path not found: ${repoPath}`);
339
+ process.exit(1);
340
+ }
341
+ }
342
+
343
+ if (JSON_OUTPUT) {
344
+ const result = detectInterfacesJSON(repoPath);
345
+ console.log(JSON.stringify(result, null, 2));
346
+ if (DRY_RUN) process.exit(0);
347
+ }
348
+
349
+ await installFromPath(repoPath);
350
+ }
351
+
352
+ // ── ldm install (bare): show catalog + update registered ──
353
+
354
+ async function cmdInstallCatalog() {
355
+ const registry = readJSON(REGISTRY_PATH);
356
+ const installed = Object.keys(registry?.extensions || {});
357
+ const components = loadCatalog();
358
+
359
+ console.log('');
360
+
361
+ // Show installed
362
+ if (installed.length > 0) {
363
+ console.log(' Installed components:');
364
+ for (const name of installed) {
365
+ const info = registry.extensions[name];
366
+ console.log(` [x] ${name} v${info?.version || '?'}`);
367
+ }
368
+ console.log('');
369
+ }
370
+
371
+ // Show available (not installed)
372
+ const available = components.filter(c => !installed.includes(c.id));
373
+ if (available.length > 0) {
374
+ console.log(' Available components:');
375
+ let idx = 1;
376
+ const selectable = [];
377
+ for (const c of available) {
378
+ if (c.status === 'coming-soon') {
379
+ console.log(` [ ] ${c.name} (coming soon)`);
380
+ } else {
381
+ console.log(` [ ] ${c.name}`);
382
+ selectable.push(c);
383
+ }
384
+ idx++;
385
+ }
386
+ console.log('');
387
+
388
+ // Interactive prompt if TTY and not --yes/--none
389
+ if (selectable.length > 0 && !YES_FLAG && !NONE_FLAG && !DRY_RUN && process.stdin.isTTY) {
390
+ const { createInterface } = await import('node:readline');
391
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
392
+ const answer = await new Promise((resolve) => {
393
+ rl.question(' Install more? [number,all,none]: ', (a) => {
394
+ rl.close();
395
+ resolve(a.trim().toLowerCase());
396
+ });
397
+ });
398
+
399
+ if (answer && answer !== 'none' && answer !== 'n') {
400
+ let toInstall = [];
401
+ if (answer === 'all' || answer === 'a') {
402
+ toInstall = selectable;
403
+ } else {
404
+ const nums = answer.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
405
+ toInstall = nums.map(n => selectable[n - 1]).filter(Boolean);
406
+ }
407
+ for (const c of toInstall) {
408
+ console.log(` Installing ${c.name}...`);
409
+ try {
410
+ execSync(`ldm install ${c.repo}`, { stdio: 'inherit' });
411
+ } catch (e) {
412
+ console.error(` x Failed to install ${c.name}: ${e.message}`);
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ // Update installed extensions
420
+ if (installed.length > 0) {
421
+ await cmdUpdateAll();
422
+ }
423
+
424
+ if (installed.length === 0 && available.filter(c => c.status !== 'coming-soon').length === 0) {
425
+ console.log(' No extensions registered. Nothing to update.');
426
+ console.log(' Use: ldm install <org/repo> or ldm install /path/to/repo');
427
+ console.log('');
428
+ }
429
+ }
430
+
431
+ async function cmdUpdateAll() {
432
+ const registry = readJSON(REGISTRY_PATH);
433
+ if (!registry?.extensions || Object.keys(registry.extensions).length === 0) return;
434
+
435
+ const { setFlags, installFromPath } = await import('../lib/deploy.mjs');
436
+ setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT });
437
+
438
+ const extensions = Object.entries(registry.extensions);
439
+ console.log(` Updating ${extensions.length} registered extension(s)...`);
440
+ console.log('');
441
+
442
+ let updated = 0;
443
+ for (const [name, info] of extensions) {
444
+ const source = info.source;
445
+ if (!source || !existsSync(source)) {
446
+ console.log(` x ${name}: source not found at ${source || '(none)'}`);
447
+ continue;
448
+ }
449
+
450
+ await installFromPath(source);
451
+ updated++;
452
+ }
453
+
454
+ console.log('');
455
+ console.log(` Updated ${updated}/${extensions.length} extension(s).`);
456
+ console.log('');
457
+ }
458
+
459
+ // ── ldm doctor ──
460
+
461
+ function cmdDoctor() {
462
+ console.log('');
463
+ console.log(' ldm doctor');
464
+ console.log(' ────────────────────────────────────');
465
+
466
+ let issues = 0;
467
+
468
+ // 1. Check LDM root
469
+ if (!existsSync(LDM_ROOT)) {
470
+ console.log(' x ~/.ldm/ does not exist. Run: ldm init');
471
+ issues++;
472
+ } else {
473
+ console.log(' + ~/.ldm/ exists');
474
+ }
475
+
476
+ // 2. Check version.json
477
+ const version = readJSON(VERSION_PATH);
478
+ if (!version) {
479
+ console.log(' x version.json missing. Run: ldm init');
480
+ issues++;
481
+ } else {
482
+ console.log(` + version.json: v${version.version} (installed ${version.installed?.split('T')[0]})`);
483
+ }
484
+
485
+ // 3. Check registry
486
+ const registry = readJSON(REGISTRY_PATH);
487
+ if (!registry) {
488
+ console.log(' x registry.json missing');
489
+ issues++;
490
+ } else {
491
+ const extCount = Object.keys(registry.extensions || {}).length;
492
+ console.log(` + registry.json: ${extCount} extension(s)`);
493
+
494
+ // Check each extension
495
+ for (const [name, info] of Object.entries(registry.extensions || {})) {
496
+ const ldmPath = info.ldmPath || join(LDM_EXTENSIONS, name);
497
+ const hasLdm = existsSync(ldmPath);
498
+ const hasPkg = hasLdm && existsSync(join(ldmPath, 'package.json'));
499
+
500
+ if (!hasLdm) {
501
+ console.log(` x ${name}: not found at ${ldmPath}`);
502
+ issues++;
503
+ } else if (!hasPkg) {
504
+ console.log(` x ${name}: deployed but missing package.json`);
505
+ issues++;
506
+ } else {
507
+ const pkg = readJSON(join(ldmPath, 'package.json'));
508
+ const ver = pkg?.version || '?';
509
+ const registeredVer = info.version || '?';
510
+ if (ver !== registeredVer) {
511
+ console.log(` ! ${name}: deployed v${ver} but registry says v${registeredVer}`);
512
+ } else {
513
+ console.log(` + ${name}: v${ver}`);
514
+ }
515
+ }
516
+
517
+ // Check OC copy
518
+ if (info.ocPath && !existsSync(info.ocPath)) {
519
+ console.log(` x OpenClaw copy missing at ${info.ocPath}`);
520
+ issues++;
521
+ }
522
+ }
523
+ }
524
+
525
+ // 4. Check sacred locations
526
+ const sacred = [
527
+ { path: join(LDM_ROOT, 'memory'), label: 'memory/' },
528
+ { path: join(LDM_ROOT, 'agents'), label: 'agents/' },
529
+ { path: join(LDM_ROOT, 'state'), label: 'state/' },
530
+ ];
531
+
532
+ for (const s of sacred) {
533
+ if (existsSync(s.path)) {
534
+ console.log(` + ${s.label} exists`);
535
+ } else {
536
+ console.log(` ! ${s.label} missing (run: ldm init)`);
537
+ }
538
+ }
539
+
540
+ // 5. Check settings.json for hooks
541
+ const settingsPath = join(HOME, '.claude', 'settings.json');
542
+ const settings = readJSON(settingsPath);
543
+ if (settings?.hooks) {
544
+ const hookCount = Object.values(settings.hooks).reduce((sum, arr) => sum + (arr?.length || 0), 0);
545
+ console.log(` + Claude Code hooks: ${hookCount} configured`);
546
+ } else {
547
+ console.log(` - Claude Code hooks: none configured`);
548
+ }
549
+
550
+ // 6. Check MCP servers
551
+ const ccUserPath = join(HOME, '.claude.json');
552
+ const ccUser = readJSON(ccUserPath);
553
+ if (ccUser?.mcpServers) {
554
+ const mcpCount = Object.keys(ccUser.mcpServers).length;
555
+ console.log(` + MCP servers (user): ${mcpCount} registered`);
556
+ }
557
+
558
+ console.log('');
559
+ if (issues === 0) {
560
+ console.log(' All checks passed.');
561
+ } else {
562
+ console.log(` ${issues} issue(s) found.`);
563
+ }
564
+ console.log('');
565
+ }
566
+
567
+ // ── ldm status ──
568
+
569
+ function cmdStatus() {
570
+ const version = readJSON(VERSION_PATH);
571
+ const registry = readJSON(REGISTRY_PATH);
572
+ const extCount = Object.keys(registry?.extensions || {}).length;
573
+
574
+ if (JSON_OUTPUT) {
575
+ console.log(JSON.stringify({
576
+ version: version?.version || null,
577
+ installed: version?.installed || null,
578
+ updated: version?.updated || null,
579
+ extensions: extCount,
580
+ ldmRoot: LDM_ROOT,
581
+ }, null, 2));
582
+ return;
583
+ }
584
+
585
+ if (!version) {
586
+ console.log(' LDM OS not installed. Run: ldm init');
587
+ return;
588
+ }
589
+
590
+ console.log('');
591
+ console.log(` LDM OS v${version.version}`);
592
+ console.log(` Installed: ${version.installed?.split('T')[0]}`);
593
+ console.log(` Updated: ${version.updated?.split('T')[0]}`);
594
+ console.log(` Extensions: ${extCount}`);
595
+ console.log(` Root: ${LDM_ROOT}`);
596
+
597
+ if (extCount > 0) {
598
+ console.log('');
599
+ for (const [name, info] of Object.entries(registry.extensions)) {
600
+ console.log(` ${name} v${info.version || '?'} (${(info.interfaces || []).join(', ')})`);
601
+ }
602
+ }
603
+
604
+ console.log('');
605
+ }
606
+
607
+ // ── Main ──
608
+
609
+ async function main() {
610
+ if (!command || command === '--help' || command === '-h') {
611
+ console.log('');
612
+ console.log(' ldm ... LDM OS kernel');
613
+ console.log('');
614
+ console.log(' Usage:');
615
+ console.log(' ldm init Scaffold ~/.ldm/ and write version.json');
616
+ console.log(' ldm install <org/repo> Install from GitHub (clones, detects, deploys)');
617
+ console.log(' ldm install /path/to/repo Install from local path');
618
+ console.log(' ldm install Update all registered extensions');
619
+ console.log(' ldm doctor Check health of all extensions');
620
+ console.log(' ldm status Show version and extension list');
621
+ console.log('');
622
+ console.log(' Flags:');
623
+ console.log(' --dry-run Show what would happen without making changes');
624
+ console.log(' --json Output results as JSON');
625
+ console.log('');
626
+ console.log(' Interfaces detected:');
627
+ console.log(' CLI ... package.json bin -> npm install -g');
628
+ console.log(' Module ... ESM main/exports -> importable');
629
+ console.log(' MCP Server ... mcp-server.mjs -> claude mcp add --scope user');
630
+ console.log(' OpenClaw ... openclaw.plugin.json -> ~/.ldm/extensions/ + ~/.openclaw/extensions/');
631
+ console.log(' Skill ... SKILL.md -> ~/.openclaw/skills/<tool>/');
632
+ console.log(' CC Hook ... guard.mjs or claudeCode.hook -> ~/.claude/settings.json');
633
+ console.log('');
634
+ console.log(` v${PKG_VERSION}`);
635
+ console.log('');
636
+ process.exit(0);
637
+ }
638
+
639
+ if (command === '--version' || command === '-v') {
640
+ console.log(PKG_VERSION);
641
+ process.exit(0);
642
+ }
643
+
644
+ switch (command) {
645
+ case 'init':
646
+ await cmdInit();
647
+ break;
648
+ case 'install':
649
+ await cmdInstall();
650
+ break;
651
+ case 'update':
652
+ // Alias: `ldm update` = `ldm install` (update all registered)
653
+ await cmdUpdateAll();
654
+ break;
655
+ case 'doctor':
656
+ cmdDoctor();
657
+ break;
658
+ case 'status':
659
+ cmdStatus();
660
+ break;
661
+ default:
662
+ console.error(` Unknown command: ${command}`);
663
+ console.error(` Run: ldm --help`);
664
+ process.exit(1);
665
+ }
666
+ }
667
+
668
+ main().catch(e => {
669
+ console.error(` x ${e.message}`);
670
+ process.exit(1);
671
+ });