@wipcomputer/wip-ldm-os 0.4.85-alpha.3 → 0.4.85-alpha.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.
- package/README.md +22 -2
- package/SKILL.md +136 -14
- package/bin/ldm.js +422 -75
- package/docs/universal-installer/SPEC.md +16 -3
- package/docs/universal-installer/TECHNICAL.md +4 -4
- package/lib/deploy.mjs +104 -20
- package/lib/detect.mjs +35 -4
- package/lib/registry-migrations.mjs +296 -0
- package/package.json +17 -2
- package/scripts/test-crc-agentid-tenant-boundary.mjs +80 -0
- package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
- package/scripts/test-crc-e2ee-session-route.mjs +129 -0
- package/scripts/test-crc-pair-login-flow.mjs +40 -0
- package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
- package/scripts/test-crc-pair-status-poll-token.mjs +73 -0
- package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
- package/scripts/test-install-prompt-policy.mjs +84 -0
- package/scripts/test-installer-skill-directory.mjs +55 -0
- package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
- package/scripts/test-installer-target-self-update.mjs +131 -0
- package/scripts/test-ldm-status-concurrency.mjs +118 -0
- package/scripts/test-ldm-status-timeout.mjs +96 -0
- package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
- package/scripts/test-readme-install-prompt.mjs +66 -0
- package/shared/templates/install-prompt.md +20 -2
- package/src/hosted-mcp/README.md +37 -0
- package/src/hosted-mcp/app/footer.js +74 -0
- package/src/hosted-mcp/app/kaleidoscope-login.html +846 -0
- package/src/hosted-mcp/app/pair.html +165 -57
- package/src/hosted-mcp/app/sprites.png +0 -0
- package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
- package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
- package/src/hosted-mcp/demo/index.html +3 -7
- package/src/hosted-mcp/demo/login.html +318 -20
- package/src/hosted-mcp/deploy.sh +308 -56
- package/src/hosted-mcp/docs/self-host.md +268 -0
- package/src/hosted-mcp/nginx/codex-relay.conf +25 -0
- package/src/hosted-mcp/nginx/conf.d/redact-logs.conf +60 -0
- package/src/hosted-mcp/nginx/mcp-oauth.conf +58 -0
- package/src/hosted-mcp/nginx/wip.computer.conf +25 -1
- package/src/hosted-mcp/scripts/audit-logs.sh +205 -0
- package/src/hosted-mcp/scripts/verify-deploy.sh +102 -0
- package/src/hosted-mcp/server.mjs +1034 -146
package/bin/ldm.js
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* ldm --version Show version
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync, lstatSync, symlinkSync } from 'node:fs';
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync, lstatSync, symlinkSync, copyFileSync } from 'node:fs';
|
|
24
24
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
25
|
-
import { execSync } from 'node:child_process';
|
|
25
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
|
|
28
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -211,6 +211,11 @@ function writeJSON(path, data) {
|
|
|
211
211
|
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
function parsePositiveInt(value, fallback) {
|
|
215
|
+
const parsed = Number.parseInt(value || '', 10);
|
|
216
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
217
|
+
}
|
|
218
|
+
|
|
214
219
|
// ── CLI version check (#29) ──
|
|
215
220
|
|
|
216
221
|
function checkCliVersion() {
|
|
@@ -229,6 +234,62 @@ function checkCliVersion() {
|
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
|
|
237
|
+
function selectedLdmNpmTrack() {
|
|
238
|
+
return ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function selectedLdmTrackLabel(npmTag) {
|
|
242
|
+
return npmTag === 'latest' ? '' : ` (${npmTag} track)`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function latestLdmCliForSelectedTrack() {
|
|
246
|
+
const npmTag = selectedLdmNpmTrack();
|
|
247
|
+
const npmViewCmd = npmTag === 'latest'
|
|
248
|
+
? 'npm view @wipcomputer/wip-ldm-os version 2>/dev/null'
|
|
249
|
+
: `npm view @wipcomputer/wip-ldm-os dist-tags.${npmTag} 2>/dev/null`;
|
|
250
|
+
const latest = execSync(npmViewCmd, {
|
|
251
|
+
encoding: 'utf8',
|
|
252
|
+
timeout: 15000,
|
|
253
|
+
}).trim();
|
|
254
|
+
return { latest, npmTag };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function maybeSelfUpdateLdmCliBeforeInstall() {
|
|
258
|
+
// This shared preflight covers both bare `ldm install` and targeted
|
|
259
|
+
// `ldm install <app>`. Dry runs never update, but still disclose skew.
|
|
260
|
+
if (process.env.LDM_SELF_UPDATED) return;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const { latest, npmTag } = latestLdmCliForSelectedTrack();
|
|
264
|
+
if (!latest || !semverNewer(latest, PKG_VERSION)) return;
|
|
265
|
+
|
|
266
|
+
const trackLabel = selectedLdmTrackLabel(npmTag);
|
|
267
|
+
|
|
268
|
+
if (DRY_RUN) {
|
|
269
|
+
console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel} is available.`);
|
|
270
|
+
console.log(` Dry run only: continuing with v${PKG_VERSION}.`);
|
|
271
|
+
console.log('');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel}. Updating first...`);
|
|
276
|
+
try {
|
|
277
|
+
execSync(`npm install -g @wipcomputer/wip-ldm-os@${latest}`, { stdio: 'inherit', timeout: 60000 });
|
|
278
|
+
console.log(` CLI updated to v${latest}. Re-running with new code...`);
|
|
279
|
+
console.log('');
|
|
280
|
+
const reArgs = process.argv.slice(2);
|
|
281
|
+
const child = spawnSync('ldm', reArgs.length > 0 ? reArgs : ['install'], {
|
|
282
|
+
stdio: 'inherit',
|
|
283
|
+
env: { ...process.env, LDM_SELF_UPDATED: '1' },
|
|
284
|
+
});
|
|
285
|
+
if (child.error) throw child.error;
|
|
286
|
+
process.exit(child.status ?? 1);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.log(` ! Self-update failed: ${e.message}. Continuing with v${PKG_VERSION}.`);
|
|
289
|
+
}
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
|
|
232
293
|
// ── Dead backup trigger cleanup (#207) ──
|
|
233
294
|
// Three backup systems were competing. Only ai.openclaw.ldm-backup (3am) works.
|
|
234
295
|
// This removes: broken cron entry (LDMDevTools.app), old com.wipcomputer.daily-backup.
|
|
@@ -1440,23 +1501,6 @@ async function showCatalogPicker() {
|
|
|
1440
1501
|
// ── ldm install ──
|
|
1441
1502
|
|
|
1442
1503
|
async function cmdInstall() {
|
|
1443
|
-
if (!DRY_RUN && !acquireInstallLock()) return;
|
|
1444
|
-
|
|
1445
|
-
// Ensure LDM is initialized
|
|
1446
|
-
if (!existsSync(VERSION_PATH)) {
|
|
1447
|
-
console.log(' LDM OS not initialized. Running init first...');
|
|
1448
|
-
console.log('');
|
|
1449
|
-
cmdInit();
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
const { setFlags, installFromPath, installSingleTool, installToolbox, detectHarnesses } = await import('../lib/deploy.mjs');
|
|
1453
|
-
const { detectInterfacesJSON } = await import('../lib/detect.mjs');
|
|
1454
|
-
|
|
1455
|
-
// Refresh harness detection (catches newly installed harnesses)
|
|
1456
|
-
detectHarnesses();
|
|
1457
|
-
|
|
1458
|
-
setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT, origin: 'manual' });
|
|
1459
|
-
|
|
1460
1504
|
// --help flag (#81)
|
|
1461
1505
|
if (args.includes('--help') || args.includes('-h')) {
|
|
1462
1506
|
console.log(`
|
|
@@ -1476,6 +1520,25 @@ async function cmdInstall() {
|
|
|
1476
1520
|
process.exit(0);
|
|
1477
1521
|
}
|
|
1478
1522
|
|
|
1523
|
+
maybeSelfUpdateLdmCliBeforeInstall();
|
|
1524
|
+
|
|
1525
|
+
if (!DRY_RUN && !acquireInstallLock()) return;
|
|
1526
|
+
|
|
1527
|
+
// Ensure LDM is initialized
|
|
1528
|
+
if (!existsSync(VERSION_PATH)) {
|
|
1529
|
+
console.log(' LDM OS not initialized. Running init first...');
|
|
1530
|
+
console.log('');
|
|
1531
|
+
cmdInit();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const { setFlags, installFromPath, installSingleTool, installToolbox, detectHarnesses } = await import('../lib/deploy.mjs');
|
|
1535
|
+
const { detectInterfacesJSON } = await import('../lib/detect.mjs');
|
|
1536
|
+
|
|
1537
|
+
// Refresh harness detection (catches newly installed harnesses)
|
|
1538
|
+
detectHarnesses();
|
|
1539
|
+
|
|
1540
|
+
setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT, origin: 'manual' });
|
|
1541
|
+
|
|
1479
1542
|
// Find the target (skip flags)
|
|
1480
1543
|
const target = args.slice(1).find(a => !a.startsWith('--'));
|
|
1481
1544
|
|
|
@@ -1736,6 +1799,135 @@ function migrateRegistry() {
|
|
|
1736
1799
|
return migrated;
|
|
1737
1800
|
}
|
|
1738
1801
|
|
|
1802
|
+
// ── Legacy source.npm honest cleanup (Phase 1 of source-types refactor) ──
|
|
1803
|
+
// See ai/product/bugs/installer/2026-05-13--cc-mini--installer-source-npm-honest-cleanup.md
|
|
1804
|
+
|
|
1805
|
+
async function migrateLegacyNpmSources({ dryRun = false } = {}) {
|
|
1806
|
+
const {
|
|
1807
|
+
planLegacyNpmSourcesMigration,
|
|
1808
|
+
summaryHasChanges,
|
|
1809
|
+
npmPackageExists,
|
|
1810
|
+
executeDirectoryMoves,
|
|
1811
|
+
} = await import('../lib/registry-migrations.mjs');
|
|
1812
|
+
|
|
1813
|
+
const registry = readJSON(REGISTRY_PATH);
|
|
1814
|
+
if (!registry?.extensions) return null;
|
|
1815
|
+
|
|
1816
|
+
const { newRegistry, summary } = await planLegacyNpmSourcesMigration({
|
|
1817
|
+
registry,
|
|
1818
|
+
probeNpm: (name) => npmPackageExists(name, { timeoutMs: 2000 }),
|
|
1819
|
+
// Resolve the entry's on-disk location before declaring it a phantom.
|
|
1820
|
+
// Entries autoregistered with `ldmPath` (bin/ldm.js:1822 path) or with
|
|
1821
|
+
// a `paths.ldm` field (lib/deploy.mjs path) can live outside the
|
|
1822
|
+
// default ~/.ldm/extensions/<name> location. A naive check would
|
|
1823
|
+
// silently delete a working entry as "phantom."
|
|
1824
|
+
extensionExists: (name, entry) => {
|
|
1825
|
+
const customPath = entry?.paths?.ldm || entry?.ldmPath;
|
|
1826
|
+
if (customPath && existsSync(customPath)) return true;
|
|
1827
|
+
return existsSync(join(LDM_EXTENSIONS, name));
|
|
1828
|
+
},
|
|
1829
|
+
now: () => new Date(),
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
if (!summaryHasChanges(summary)) return summary;
|
|
1833
|
+
|
|
1834
|
+
if (!dryRun) {
|
|
1835
|
+
const stamp = summary.timestamp.replace(/[:.]/g, '-');
|
|
1836
|
+
const backupPath = `${REGISTRY_PATH}.bak-${stamp}`;
|
|
1837
|
+
copyFileSync(REGISTRY_PATH, backupPath);
|
|
1838
|
+
summary.backupPath = backupPath;
|
|
1839
|
+
writeJSON(REGISTRY_PATH, newRegistry);
|
|
1840
|
+
|
|
1841
|
+
// Execute directory moves AFTER the registry write. Each move sends a
|
|
1842
|
+
// deduplicated extension's on-disk directory to ~/.ldm/_trash/<name>-
|
|
1843
|
+
// deduplicated-<timestamp> so autoDetectExtensions (which only scans
|
|
1844
|
+
// ~/.ldm/extensions/) cannot find it on the next install pass.
|
|
1845
|
+
// Without this, the registry dedup reverts within the same install run.
|
|
1846
|
+
// See ai/product/bugs/installer/2026-05-13--cc-mini--installer-dedup-reverts-between-installs.md
|
|
1847
|
+
const { performed, skipped } = executeDirectoryMoves({
|
|
1848
|
+
directoryMoves: summary.directoryMoves,
|
|
1849
|
+
extensionsRoot: LDM_EXTENSIONS,
|
|
1850
|
+
trashRoot: join(LDM_ROOT, '_trash'),
|
|
1851
|
+
});
|
|
1852
|
+
summary.directoryMovesPerformed.push(...performed);
|
|
1853
|
+
summary.directoryMovesSkipped.push(...skipped);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
return summary;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function printLegacyNpmSourcesSummary(summary, { dryRun = false } = {}) {
|
|
1860
|
+
if (!summary) return;
|
|
1861
|
+
const writeableChanges =
|
|
1862
|
+
summary.migrated.length +
|
|
1863
|
+
summary.phantomsRemoved.length +
|
|
1864
|
+
summary.duplicatesRemoved.length;
|
|
1865
|
+
const probeFailureCount = summary.probeFailures?.length || 0;
|
|
1866
|
+
// Always surface probe failures even when nothing was writeable. If every
|
|
1867
|
+
// probe times out, the install would otherwise look like a no-op and the
|
|
1868
|
+
// [unavailable] rows in `ldm status` would survive silently.
|
|
1869
|
+
if (writeableChanges === 0 && probeFailureCount === 0) return;
|
|
1870
|
+
|
|
1871
|
+
console.log('');
|
|
1872
|
+
console.log(dryRun
|
|
1873
|
+
? ' Registry source.npm cleanup (dry run, no changes written):'
|
|
1874
|
+
: ' Registry source.npm cleanup:');
|
|
1875
|
+
|
|
1876
|
+
if (summary.migrated.length > 0) {
|
|
1877
|
+
console.log(` + ${dryRun ? 'Would migrate' : 'Migrated'} ${summary.migrated.length} entr${summary.migrated.length === 1 ? 'y' : 'ies'} to updateSource.type=untracked`);
|
|
1878
|
+
for (const m of summary.migrated) {
|
|
1879
|
+
const detail = m.legacyNpmName
|
|
1880
|
+
? ` (legacy source.npm "${m.legacyNpmName}" preserved in provenance)`
|
|
1881
|
+
: ' (no source info; classified as untracked)';
|
|
1882
|
+
console.log(` ${m.name}${detail}`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (summary.phantomsRemoved.length > 0) {
|
|
1886
|
+
console.log(` + ${dryRun ? 'Would remove' : 'Removed'} ${summary.phantomsRemoved.length} phantom entr${summary.phantomsRemoved.length === 1 ? 'y' : 'ies'}:`);
|
|
1887
|
+
for (const p of summary.phantomsRemoved) {
|
|
1888
|
+
console.log(` ${p.name} (${p.reason})`);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
if (summary.duplicatesRemoved.length > 0) {
|
|
1892
|
+
console.log(` + ${dryRun ? 'Would remove' : 'Removed'} ${summary.duplicatesRemoved.length} duplicate entr${summary.duplicatesRemoved.length === 1 ? 'y' : 'ies'}:`);
|
|
1893
|
+
for (const d of summary.duplicatesRemoved) {
|
|
1894
|
+
console.log(` ${d.removed} (canonical: ${d.keep})`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (summary.directoryMoves && summary.directoryMoves.length > 0) {
|
|
1898
|
+
if (dryRun) {
|
|
1899
|
+
console.log(` + Would move ${summary.directoryMoves.length} duplicate director${summary.directoryMoves.length === 1 ? 'y' : 'ies'} to ~/.ldm/_trash/ (so autoDetectExtensions cannot re-register):`);
|
|
1900
|
+
for (const m of summary.directoryMoves) {
|
|
1901
|
+
console.log(` ~/.ldm/extensions/${m.name} -> ~/.ldm/_trash/${m.trashName}`);
|
|
1902
|
+
}
|
|
1903
|
+
} else {
|
|
1904
|
+
const performed = summary.directoryMovesPerformed || [];
|
|
1905
|
+
const skipped = summary.directoryMovesSkipped || [];
|
|
1906
|
+
if (performed.length > 0) {
|
|
1907
|
+
console.log(` + Moved ${performed.length} duplicate director${performed.length === 1 ? 'y' : 'ies'} to ~/.ldm/_trash/ (autoDetectExtensions cannot re-register):`);
|
|
1908
|
+
for (const m of performed) {
|
|
1909
|
+
console.log(` ${m.name} -> ${m.destPath.replace(HOME, '~')}`);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (skipped.length > 0) {
|
|
1913
|
+
console.log(` ! ${skipped.length} duplicate director${skipped.length === 1 ? 'y' : 'ies'} could not be moved (registry entries are removed; autoDetect may re-add on next install):`);
|
|
1914
|
+
for (const m of skipped) {
|
|
1915
|
+
console.log(` ${m.name} (${m.reason})`);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (summary.probeFailures && summary.probeFailures.length > 0) {
|
|
1921
|
+
console.log(` ! ${summary.probeFailures.length} npm probe${summary.probeFailures.length === 1 ? '' : 's'} could not complete (will retry on next install):`);
|
|
1922
|
+
for (const f of summary.probeFailures) {
|
|
1923
|
+
console.log(` ${f.name} (source.npm "${f.npmName}")`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (!dryRun && summary.backupPath) {
|
|
1927
|
+
console.log(` + Registry backup: ${summary.backupPath.replace(HOME, '~')}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1739
1931
|
// ── Auto-detect unregistered extensions ──
|
|
1740
1932
|
|
|
1741
1933
|
function autoDetectExtensions() {
|
|
@@ -1921,38 +2113,6 @@ async function cmdInstallCatalog() {
|
|
|
1921
2113
|
// No lock here. cmdInstall() already holds it when calling this.
|
|
1922
2114
|
installLog(`ldm install started (v${PKG_VERSION}, DRY_RUN=${DRY_RUN})`);
|
|
1923
2115
|
|
|
1924
|
-
// Self-update: check if CLI itself is outdated. Update first, then re-exec.
|
|
1925
|
-
// This breaks the chicken-and-egg: new features in ldm install are always
|
|
1926
|
-
// available because the installer upgrades itself before doing anything else.
|
|
1927
|
-
// --alpha and --beta flags check the corresponding npm dist-tag instead of @latest.
|
|
1928
|
-
if (!DRY_RUN && !process.env.LDM_SELF_UPDATED) {
|
|
1929
|
-
try {
|
|
1930
|
-
const npmTag = ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';
|
|
1931
|
-
const trackLabel = npmTag === 'latest' ? '' : ` (${npmTag} track)`;
|
|
1932
|
-
const npmViewCmd = npmTag === 'latest'
|
|
1933
|
-
? 'npm view @wipcomputer/wip-ldm-os version 2>/dev/null'
|
|
1934
|
-
: `npm view @wipcomputer/wip-ldm-os dist-tags.${npmTag} 2>/dev/null`;
|
|
1935
|
-
const latest = execSync(npmViewCmd, {
|
|
1936
|
-
encoding: 'utf8', timeout: 15000,
|
|
1937
|
-
}).trim();
|
|
1938
|
-
if (latest && semverNewer(latest, PKG_VERSION)) {
|
|
1939
|
-
console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel}. Updating first...`);
|
|
1940
|
-
try {
|
|
1941
|
-
execSync(`npm install -g @wipcomputer/wip-ldm-os@${latest}`, { stdio: 'inherit', timeout: 60000 });
|
|
1942
|
-
console.log(` CLI updated to v${latest}. Re-running with new code...`);
|
|
1943
|
-
console.log('');
|
|
1944
|
-
// Re-exec with the new binary. LDM_SELF_UPDATED prevents infinite loop.
|
|
1945
|
-
// process.argv.slice(2) skips 'node' and the script path, keeps just 'install' + flags
|
|
1946
|
-
const reArgs = process.argv.slice(2).join(' ') || 'install';
|
|
1947
|
-
execSync(`LDM_SELF_UPDATED=1 ldm ${reArgs}`, { stdio: 'inherit' });
|
|
1948
|
-
process.exit(0);
|
|
1949
|
-
} catch (e) {
|
|
1950
|
-
console.log(` ! Self-update failed: ${e.message}. Continuing with v${PKG_VERSION}.`);
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
} catch {}
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
2116
|
autoDetectExtensions();
|
|
1957
2117
|
|
|
1958
2118
|
// Migrate old registry entries to v2 format (#262)
|
|
@@ -1961,6 +2121,12 @@ async function cmdInstallCatalog() {
|
|
|
1961
2121
|
console.log(` + Migrated ${migrated} registry entries to v2 format (source info added)`);
|
|
1962
2122
|
}
|
|
1963
2123
|
|
|
2124
|
+
// Phase 1 of source-types refactor: clear bad source.npm entries, dedupe,
|
|
2125
|
+
// and classify mystery rows as untracked so `ldm status` stops lying.
|
|
2126
|
+
// See ai/product/bugs/installer/2026-05-13--cc-mini--installer-source-npm-honest-cleanup.md
|
|
2127
|
+
const npmCleanupSummary = await migrateLegacyNpmSources({ dryRun: DRY_RUN });
|
|
2128
|
+
printLegacyNpmSourcesSummary(npmCleanupSummary, { dryRun: DRY_RUN });
|
|
2129
|
+
|
|
1964
2130
|
// Aggregate the bin ownership manifest BEFORE seedLocalCatalog,
|
|
1965
2131
|
// deployBridge, deployScripts, and the heal walk run. If two
|
|
1966
2132
|
// declarers claim the same file in ~/.ldm/bin/ we cannot safely
|
|
@@ -2891,6 +3057,27 @@ async function cmdDoctor() {
|
|
|
2891
3057
|
}
|
|
2892
3058
|
}
|
|
2893
3059
|
|
|
3060
|
+
// Warn on registry entries whose legacy source.npm value 404s on npm.
|
|
3061
|
+
// `ldm install` migrates these to updateSource.type=untracked; this catches
|
|
3062
|
+
// future drift and any entries the install-time probe couldn't reach.
|
|
3063
|
+
// See ai/product/bugs/installer/2026-05-13--cc-mini--installer-source-npm-honest-cleanup.md
|
|
3064
|
+
try {
|
|
3065
|
+
const { findLegacyNpm404Entries, npmPackageExists } = await import('../lib/registry-migrations.mjs');
|
|
3066
|
+
const docRegistry = readJSON(REGISTRY_PATH);
|
|
3067
|
+
const legacy404 = await findLegacyNpm404Entries({
|
|
3068
|
+
registry: docRegistry,
|
|
3069
|
+
probeNpm: (name) => npmPackageExists(name, { timeoutMs: 1500 }),
|
|
3070
|
+
});
|
|
3071
|
+
if (legacy404.length > 0) {
|
|
3072
|
+
console.log('');
|
|
3073
|
+
console.log(` ! ${legacy404.length} extension(s) declare an npm source whose package does not exist on npm:`);
|
|
3074
|
+
for (const { name, npmName } of legacy404) {
|
|
3075
|
+
console.log(` ${name} (source.npm "${npmName}")`);
|
|
3076
|
+
}
|
|
3077
|
+
console.log(' Run `ldm install` to migrate these to updateSource.type=untracked.');
|
|
3078
|
+
}
|
|
3079
|
+
} catch {}
|
|
3080
|
+
|
|
2894
3081
|
// --fix: clean up registered-missing entries
|
|
2895
3082
|
if (FIX_FLAG && registeredMissing.length > 0) {
|
|
2896
3083
|
const registry = readJSON(REGISTRY_PATH);
|
|
@@ -3251,7 +3438,91 @@ async function cmdDoctor() {
|
|
|
3251
3438
|
|
|
3252
3439
|
// ── ldm status ──
|
|
3253
3440
|
|
|
3254
|
-
|
|
3441
|
+
const STATUS_NPM_TIMEOUT_MS = parsePositiveInt(process.env.LDM_STATUS_NPM_TIMEOUT_MS, 5000);
|
|
3442
|
+
const STATUS_TOTAL_BUDGET_MS = parsePositiveInt(process.env.LDM_STATUS_TOTAL_BUDGET_MS, 60000);
|
|
3443
|
+
const STATUS_NPM_CONCURRENCY = parsePositiveInt(process.env.LDM_STATUS_NPM_CONCURRENCY, 8);
|
|
3444
|
+
const STATUS_NPM_REGISTRY_URL = process.env.LDM_STATUS_NPM_REGISTRY_URL || 'https://registry.npmjs.org';
|
|
3445
|
+
|
|
3446
|
+
async function npmViewVersionForStatus(pkg, timeoutMs) {
|
|
3447
|
+
const controller = new AbortController();
|
|
3448
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3449
|
+
try {
|
|
3450
|
+
const registry = STATUS_NPM_REGISTRY_URL.replace(/\/+$/, '');
|
|
3451
|
+
const response = await fetch(`${registry}/${encodeURIComponent(pkg)}`, {
|
|
3452
|
+
signal: controller.signal,
|
|
3453
|
+
headers: { accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
3454
|
+
});
|
|
3455
|
+
if (!response.ok) {
|
|
3456
|
+
const error = new Error(`npm registry returned ${response.status}`);
|
|
3457
|
+
error.statusCode = response.status;
|
|
3458
|
+
throw error;
|
|
3459
|
+
}
|
|
3460
|
+
const metadata = await response.json();
|
|
3461
|
+
return metadata?.['dist-tags']?.latest || '';
|
|
3462
|
+
} finally {
|
|
3463
|
+
clearTimeout(timeout);
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
function remainingStatusBudgetMs(startedAt) {
|
|
3468
|
+
return Math.max(0, STATUS_TOTAL_BUDGET_MS - (Date.now() - startedAt));
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
function formatStatusElapsed(ms) {
|
|
3472
|
+
if (!Number.isFinite(ms) || ms <= 0) return '0ms';
|
|
3473
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
3474
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
function classifyStatusCheckError(error) {
|
|
3478
|
+
if (error?.name === 'AbortError' || error?.signal === 'SIGTERM' || error?.code === 'ETIMEDOUT' || String(error?.message || '').includes('ETIMEDOUT')) {
|
|
3479
|
+
return 'timeout';
|
|
3480
|
+
}
|
|
3481
|
+
return 'unavailable';
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
async function runStatusProbesWithConcurrency(items, concurrency, statusStartedAt) {
|
|
3485
|
+
if (items.length === 0) return [];
|
|
3486
|
+
|
|
3487
|
+
const results = new Array(items.length);
|
|
3488
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
3489
|
+
let nextIndex = 0;
|
|
3490
|
+
|
|
3491
|
+
async function worker() {
|
|
3492
|
+
while (nextIndex < items.length) {
|
|
3493
|
+
const index = nextIndex;
|
|
3494
|
+
nextIndex += 1;
|
|
3495
|
+
const item = items[index];
|
|
3496
|
+
const remaining = remainingStatusBudgetMs(statusStartedAt);
|
|
3497
|
+
|
|
3498
|
+
if (remaining <= 0) {
|
|
3499
|
+
results[index] = { ...item, status: 'skipped', reason: 'budget', elapsedMs: 0 };
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
const timeout = Math.min(STATUS_NPM_TIMEOUT_MS, remaining);
|
|
3504
|
+
const probeStartedAt = Date.now();
|
|
3505
|
+
console.log(` ${item.name}: checking npm`);
|
|
3506
|
+
|
|
3507
|
+
try {
|
|
3508
|
+
const latest = await npmViewVersionForStatus(item.npm, timeout);
|
|
3509
|
+
results[index] = { ...item, status: 'ok', latest, elapsedMs: Date.now() - probeStartedAt };
|
|
3510
|
+
} catch (error) {
|
|
3511
|
+
results[index] = {
|
|
3512
|
+
...item,
|
|
3513
|
+
status: 'skipped',
|
|
3514
|
+
reason: classifyStatusCheckError(error),
|
|
3515
|
+
elapsedMs: Date.now() - probeStartedAt,
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
3522
|
+
return results;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
async function cmdStatus() {
|
|
3255
3526
|
const version = readJSON(VERSION_PATH);
|
|
3256
3527
|
const registry = readJSON(REGISTRY_PATH);
|
|
3257
3528
|
const extCount = Object.keys(registry?.extensions || {}).length;
|
|
@@ -3272,18 +3543,46 @@ function cmdStatus() {
|
|
|
3272
3543
|
return;
|
|
3273
3544
|
}
|
|
3274
3545
|
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3546
|
+
console.log('');
|
|
3547
|
+
console.log(` LDM OS v${version.version}`);
|
|
3548
|
+
console.log(` Installed: ${version.installed?.split('T')[0] || 'unknown'}`);
|
|
3549
|
+
console.log(` Updated: ${version.updated?.split('T')[0] || 'unknown'}`);
|
|
3550
|
+
console.log(` Extensions: ${extCount}`);
|
|
3551
|
+
console.log(` Root: ${LDM_ROOT}`);
|
|
3552
|
+
|
|
3553
|
+
const statusStartedAt = Date.now();
|
|
3554
|
+
const skipped = [];
|
|
3555
|
+
|
|
3556
|
+
console.log('');
|
|
3557
|
+
console.log(' Checking updates:');
|
|
3283
3558
|
|
|
3284
3559
|
// Check extensions against npm using registry source info (#262)
|
|
3560
|
+
const probeItems = [{
|
|
3561
|
+
kind: 'cli',
|
|
3562
|
+
name: 'ldm cli',
|
|
3563
|
+
npm: '@wipcomputer/wip-ldm-os',
|
|
3564
|
+
current: PKG_VERSION,
|
|
3565
|
+
}];
|
|
3566
|
+
|
|
3285
3567
|
const updates = [];
|
|
3568
|
+
const untrackedEntries = [];
|
|
3286
3569
|
for (const [name, info] of Object.entries(registry?.extensions || {})) {
|
|
3570
|
+
// Phase 1 of source-types refactor: entries explicitly marked as untracked
|
|
3571
|
+
// are listed in a separate section and never probed. Honest reporting:
|
|
3572
|
+
// we don't know how to check their source yet.
|
|
3573
|
+
if (info?.updateSource?.type === 'untracked') {
|
|
3574
|
+
const currentVersion = info?.installed?.version || info.version || 'unknown';
|
|
3575
|
+
untrackedEntries.push({ name, version: currentVersion });
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
// Defensive skip for any other Phase 2 updateSource types (git, bundled,
|
|
3579
|
+
// private, etc.) that may appear in the registry before their probe
|
|
3580
|
+
// logic ships. Falling through would use the legacy info.source.npm
|
|
3581
|
+
// path, which is wrong for these types and would print them as
|
|
3582
|
+
// [unavailable]. Phase 2 replaces this skip with proper dispatch.
|
|
3583
|
+
if (info?.updateSource && info.updateSource.type !== 'npm') {
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3287
3586
|
// Use registry source.npm (v2) or fall back to extension's package.json
|
|
3288
3587
|
let npmPkg = info?.source?.npm || null;
|
|
3289
3588
|
if (!npmPkg) {
|
|
@@ -3294,22 +3593,52 @@ function cmdStatus() {
|
|
|
3294
3593
|
if (!npmPkg) continue;
|
|
3295
3594
|
const currentVersion = info?.installed?.version || info.version;
|
|
3296
3595
|
if (!currentVersion) continue;
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3596
|
+
probeItems.push({
|
|
3597
|
+
kind: 'extension',
|
|
3598
|
+
name,
|
|
3599
|
+
npm: npmPkg,
|
|
3600
|
+
current: currentVersion,
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
untrackedEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
3604
|
+
|
|
3605
|
+
let cliUpdate = null;
|
|
3606
|
+
const probeResults = await runStatusProbesWithConcurrency(probeItems, STATUS_NPM_CONCURRENCY, statusStartedAt);
|
|
3607
|
+
for (const result of probeResults) {
|
|
3608
|
+
if (!result) continue;
|
|
3609
|
+
if (result.status === 'skipped') {
|
|
3610
|
+
skipped.push({
|
|
3611
|
+
name: result.name,
|
|
3612
|
+
npm: result.npm,
|
|
3613
|
+
reason: result.reason,
|
|
3614
|
+
elapsedMs: result.elapsedMs,
|
|
3615
|
+
});
|
|
3616
|
+
continue;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
if (result.kind === 'cli') {
|
|
3620
|
+
if (result.latest && semverNewer(result.latest, PKG_VERSION)) cliUpdate = result.latest;
|
|
3621
|
+
} else if (result.latest && semverNewer(result.latest, result.current)) {
|
|
3622
|
+
updates.push({
|
|
3623
|
+
name: result.name,
|
|
3624
|
+
current: result.current,
|
|
3625
|
+
latest: result.latest,
|
|
3626
|
+
npm: result.npm,
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3305
3629
|
}
|
|
3306
3630
|
|
|
3307
3631
|
console.log('');
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3632
|
+
if (updates.length === 0 && !cliUpdate && skipped.length === 0 && untrackedEntries.length === 0) {
|
|
3633
|
+
console.log(' Update summary: all up to date');
|
|
3634
|
+
} else {
|
|
3635
|
+
const summaryParts = [];
|
|
3636
|
+
if (updates.length > 0) summaryParts.push(`${updates.length} extension update(s) available`);
|
|
3637
|
+
if (cliUpdate) summaryParts.push('CLI update available');
|
|
3638
|
+
if (untrackedEntries.length > 0) summaryParts.push(`${untrackedEntries.length} untracked`);
|
|
3639
|
+
if (skipped.length > 0) summaryParts.push(`${skipped.length} update check(s) skipped`);
|
|
3640
|
+
console.log(` Update summary: ${summaryParts.join(', ')}`);
|
|
3641
|
+
}
|
|
3313
3642
|
|
|
3314
3643
|
if (updates.length > 0) {
|
|
3315
3644
|
console.log('');
|
|
@@ -3325,6 +3654,24 @@ function cmdStatus() {
|
|
|
3325
3654
|
console.log(` CLI update: npm install -g @wipcomputer/wip-ldm-os@${cliUpdate}`);
|
|
3326
3655
|
}
|
|
3327
3656
|
|
|
3657
|
+
if (untrackedEntries.length > 0) {
|
|
3658
|
+
console.log('');
|
|
3659
|
+
console.log(' Untracked extensions (pending reclassification):');
|
|
3660
|
+
const maxNameLen = Math.max(...untrackedEntries.map(e => e.name.length));
|
|
3661
|
+
for (const e of untrackedEntries) {
|
|
3662
|
+
console.log(` ${e.name.padEnd(maxNameLen)} v${e.version}`);
|
|
3663
|
+
}
|
|
3664
|
+
console.log(' (run `ldm doctor --reclassify-sources` to classify these)');
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
if (skipped.length > 0) {
|
|
3668
|
+
console.log('');
|
|
3669
|
+
console.log(' Update checks skipped:');
|
|
3670
|
+
for (const item of skipped) {
|
|
3671
|
+
console.log(` ${item.name}: [${item.reason} ${formatStatusElapsed(item.elapsedMs)}] ${item.npm}`);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3328
3675
|
console.log('');
|
|
3329
3676
|
}
|
|
3330
3677
|
|
|
@@ -4005,7 +4352,7 @@ async function main() {
|
|
|
4005
4352
|
console.log(' Module ... ESM main/exports -> importable');
|
|
4006
4353
|
console.log(' MCP Server ... mcp-server.mjs -> claude mcp add --scope user');
|
|
4007
4354
|
console.log(' OpenClaw ... openclaw.plugin.json -> ~/.ldm/extensions/ + ~/.openclaw/extensions/');
|
|
4008
|
-
console.log(' Skill ... SKILL.md
|
|
4355
|
+
console.log(' Skill ... SKILL.md or skills/<name>/SKILL.md -> agent skill paths');
|
|
4009
4356
|
console.log(' CC Hook ... guard.mjs or claudeCode.hook -> ~/.claude/settings.json');
|
|
4010
4357
|
console.log('');
|
|
4011
4358
|
console.log(` v${PKG_VERSION}`);
|
|
@@ -4780,7 +5127,7 @@ async function main() {
|
|
|
4780
5127
|
await cmdDoctor();
|
|
4781
5128
|
break;
|
|
4782
5129
|
case 'status':
|
|
4783
|
-
cmdStatus();
|
|
5130
|
+
await cmdStatus();
|
|
4784
5131
|
break;
|
|
4785
5132
|
case 'sessions':
|
|
4786
5133
|
await cmdSessions();
|
|
@@ -144,13 +144,15 @@ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
|
|
|
144
144
|
|
|
145
145
|
A markdown file that teaches agents when and how to use the tool. The instruction interface. Follows the [Agent Skills Spec](https://agentskills.io/specification).
|
|
146
146
|
|
|
147
|
-
**Convention:** `SKILL.md` at the repo root. YAML frontmatter
|
|
147
|
+
**Convention:** either `SKILL.md` at the repo root, or one or more skill folders at `skills/<skill-name>/SKILL.md`. YAML frontmatter includes name and description. Optional `references/`, `agents/`, `scripts/`, and `assets/` directories live beside `SKILL.md`.
|
|
148
148
|
|
|
149
149
|
**Platform variants:** Codex CLI reads `AGENTS.md` instead of `SKILL.md`, with the same role and the same content shape. Treat `AGENTS.md` as the Codex-flavored filename for this same interface, not a separate interface. A repo may ship both (or symlink one to the other) so it works in Codex and SKILL.md-aware agents.
|
|
150
150
|
|
|
151
|
-
**Detection:** `SKILL.md` exists.
|
|
151
|
+
**Detection:** `SKILL.md` exists, or at least one `skills/<skill-name>/SKILL.md` exists.
|
|
152
152
|
|
|
153
|
-
**Install:**
|
|
153
|
+
**Install:** the skill folder is deployed to every supported local agent skill surface, including OpenClaw, Codex, Claude Code, and WIP agent compatibility paths when present. If `references/` exists, it is deployed alongside SKILL.md and to `settings/docs/skills/<name>/` in the workspace.
|
|
154
|
+
|
|
155
|
+
**Npm package shape:** a public skill package can expose `SKILL.md` at package root. For example, `@wipcomputer/wip-ai-chat-ui` is sourced from a private repo folder at `design/skills/wip-ai-chat-ui/`, but publishes a tarball with `SKILL.md`, `agents/`, and `references/` at package root so `ldm install @wipcomputer/wip-ai-chat-ui` installs the skill directly.
|
|
154
156
|
|
|
155
157
|
**Structure:**
|
|
156
158
|
```
|
|
@@ -162,6 +164,17 @@ repo/
|
|
|
162
164
|
└── ...
|
|
163
165
|
```
|
|
164
166
|
|
|
167
|
+
Multiple skills can also live in one repo:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
repo/
|
|
171
|
+
└── skills/
|
|
172
|
+
└── wip-ai-chat-ui/
|
|
173
|
+
├── SKILL.md
|
|
174
|
+
├── agents/
|
|
175
|
+
└── references/
|
|
176
|
+
```
|
|
177
|
+
|
|
165
178
|
**Key rules (from Agent Skills Spec):**
|
|
166
179
|
- SKILL.md body < 5000 tokens. Process goes in SKILL.md, context goes in references/.
|
|
167
180
|
- Imperative language: "Run this command" not "This product enables..."
|
|
@@ -148,13 +148,13 @@ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
|
|
|
148
148
|
|
|
149
149
|
A markdown file that teaches agents when and how to use the tool. The instruction interface. Follows the [Agent Skills Spec](https://agentskills.io/specification).
|
|
150
150
|
|
|
151
|
-
**Convention:** `SKILL.md` at the repo root. Optional `references/`
|
|
151
|
+
**Convention:** either `SKILL.md` at the repo root, or one or more skill folders at `skills/<skill-name>/SKILL.md`. Optional `references/`, `agents/`, `scripts/`, and `assets/` directories live beside `SKILL.md`.
|
|
152
152
|
|
|
153
153
|
**Platform variants:** Codex CLI reads `AGENTS.md` with the same role and content shape. Treat as the Codex-flavored filename for this same interface, not a separate one.
|
|
154
154
|
|
|
155
|
-
**Detection:** `SKILL.md` exists.
|
|
155
|
+
**Detection:** `SKILL.md` exists, or at least one `skills/<skill-name>/SKILL.md` exists.
|
|
156
156
|
|
|
157
|
-
**Install:** `ldm install` deploys
|
|
157
|
+
**Install:** `ldm install` deploys the skill folder to every supported local agent skill surface, including OpenClaw, Codex, Claude Code, and WIP agent compatibility paths when present. If `references/` exists, it is deployed alongside and also to `settings/docs/skills/<name>/` in the workspace so all agents can read them.
|
|
158
158
|
|
|
159
159
|
**Key rules:**
|
|
160
160
|
- SKILL.md body < 5000 tokens. Process in SKILL.md, context in references/.
|
|
@@ -364,7 +364,7 @@ ldm install # update all
|
|
|
364
364
|
| `mcp-server.mjs` | MCP (local stdio) | Adds `command` + `args` entry to `.mcp.json` |
|
|
365
365
|
| `mcp.remote.url` in `package.json` | Remote MCP | Adds `url` + `transport` entry to `.mcp.json`; prints Claude Desktop hint. **Implementation in flight ([ticket](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-detection.md)).** |
|
|
366
366
|
| `openclaw.plugin.json` | OpenClaw | Copies to `~/.openclaw/extensions/` |
|
|
367
|
-
| `SKILL.md` | Skill |
|
|
367
|
+
| `SKILL.md` or `skills/<name>/SKILL.md` | Skill | Deploys skill folder to supported agent skill paths |
|
|
368
368
|
| `guard.mjs` or `claudeCode.hook` | CC Hook | Adds to `~/.claude/settings.json` |
|
|
369
369
|
| `.claude-plugin/plugin.json` | CC Plugin | Registers with Claude Code marketplace |
|
|
370
370
|
|