@wipcomputer/wip-ldm-os 0.4.79 → 0.4.80
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 +1 -1
- package/bin/ldm.js +57 -0
- package/lib/deploy.mjs +92 -3
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ license: MIT
|
|
|
9
9
|
compatibility: Requires git, npm, node. Node.js 18+.
|
|
10
10
|
metadata:
|
|
11
11
|
display-name: "LDM OS"
|
|
12
|
-
version: "0.4.
|
|
12
|
+
version: "0.4.80"
|
|
13
13
|
homepage: "https://github.com/wipcomputer/wip-ldm-os"
|
|
14
14
|
author: "Parker Todd Brooks"
|
|
15
15
|
category: infrastructure
|
package/bin/ldm.js
CHANGED
|
@@ -2892,6 +2892,63 @@ async function cmdDoctor() {
|
|
|
2892
2892
|
cleanupStaleClaudeCodeEnv();
|
|
2893
2893
|
}
|
|
2894
2894
|
|
|
2895
|
+
// 3b. MCP health check (Phase 3c)
|
|
2896
|
+
//
|
|
2897
|
+
// Walk ~/.claude.json#mcpServers. For every entry whose command is node
|
|
2898
|
+
// and whose first arg resolves under ~/.ldm/extensions/ or ~/.openclaw/
|
|
2899
|
+
// extensions/, verify the file exists and parses. Report any that do not.
|
|
2900
|
+
//
|
|
2901
|
+
// This catches the failure mode where an extension renamed its MCP file,
|
|
2902
|
+
// updated its deployed artifacts, but the old .claude.json entry still
|
|
2903
|
+
// points at a path that was rotated into _trash. Surfacing these in
|
|
2904
|
+
// doctor makes the class of failure loud instead of silent.
|
|
2905
|
+
{
|
|
2906
|
+
const ccUserPath = join(HOME, '.claude.json');
|
|
2907
|
+
const ccUser = readJSON(ccUserPath);
|
|
2908
|
+
const ldmExtRoot = LDM_EXTENSIONS;
|
|
2909
|
+
const ocExtRoot = join(HOME, '.openclaw', 'extensions');
|
|
2910
|
+
const broken = [];
|
|
2911
|
+
if (ccUser?.mcpServers) {
|
|
2912
|
+
for (const [name, cfg] of Object.entries(ccUser.mcpServers)) {
|
|
2913
|
+
if (cfg?.command !== 'node') continue;
|
|
2914
|
+
const args = Array.isArray(cfg.args) ? cfg.args : [];
|
|
2915
|
+
const first = args[0];
|
|
2916
|
+
if (!first || typeof first !== 'string') continue;
|
|
2917
|
+
const underExt = first.startsWith(ldmExtRoot + '/') || first.startsWith(ocExtRoot + '/');
|
|
2918
|
+
if (!underExt) continue;
|
|
2919
|
+
if (!existsSync(first)) {
|
|
2920
|
+
broken.push({ name, path: first, reason: 'missing' });
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
try {
|
|
2924
|
+
execSync(`node --check "${first}"`, { stdio: 'pipe', timeout: 5000 });
|
|
2925
|
+
} catch (e) {
|
|
2926
|
+
const stderr = (e && e.stderr && e.stderr.toString && e.stderr.toString().trim()) || (e && e.message) || 'unparseable';
|
|
2927
|
+
broken.push({ name, path: first, reason: 'unparseable', detail: stderr.split('\n')[0] });
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
if (broken.length > 0) {
|
|
2932
|
+
for (const b of broken) {
|
|
2933
|
+
const detail = b.detail ? ` (${b.detail})` : '';
|
|
2934
|
+
console.log(` ! MCP ${b.name}: ${b.reason} at ${b.path}${detail}`);
|
|
2935
|
+
}
|
|
2936
|
+
if (FIX_FLAG && ccUser?.mcpServers) {
|
|
2937
|
+
for (const b of broken) {
|
|
2938
|
+
delete ccUser.mcpServers[b.name];
|
|
2939
|
+
console.log(` + Removed dangling MCP: ${b.name}`);
|
|
2940
|
+
}
|
|
2941
|
+
writeFileSync(ccUserPath, JSON.stringify(ccUser, null, 2) + '\n');
|
|
2942
|
+
// --fix resolved them, do not count as outstanding issues
|
|
2943
|
+
} else {
|
|
2944
|
+
console.log(` Run: ldm doctor --fix to remove ${broken.length} dangling MCP entr${broken.length === 1 ? 'y' : 'ies'}`);
|
|
2945
|
+
issues += broken.length;
|
|
2946
|
+
}
|
|
2947
|
+
} else if (ccUser?.mcpServers && Object.keys(ccUser.mcpServers).length > 0) {
|
|
2948
|
+
console.log(` + MCP entries under LDM/OC extensions: all paths exist and parse`);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2895
2952
|
// 4. Check sacred locations
|
|
2896
2953
|
const sacred = [
|
|
2897
2954
|
{ path: join(LDM_ROOT, 'memory'), label: 'memory/' },
|
package/lib/deploy.mjs
CHANGED
|
@@ -245,9 +245,17 @@ function buildSourceInfo(repoPath, pkg) {
|
|
|
245
245
|
hasInfo = true;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// If the repo path is
|
|
249
|
-
//
|
|
250
|
-
|
|
248
|
+
// If the repo path is itself a git working tree, trust its origin URL.
|
|
249
|
+
// Previously this ran git remote unconditionally, which walks up the
|
|
250
|
+
// directory tree. For npm-sourced installs extracted under ~/.ldm/tmp/
|
|
251
|
+
// (inside the tracked ~/.ldm repo), git happily returned the parent
|
|
252
|
+
// tracking repo's remote (wipcomputer/...-system-private) as the
|
|
253
|
+
// source for every extension. Registry source.repo was therefore
|
|
254
|
+
// unreliable. Now we only consult git if repoPath itself has a .git
|
|
255
|
+
// entry (directory for normal clones, file for worktrees). If it
|
|
256
|
+
// does not, we leave source.repo unset rather than capturing an
|
|
257
|
+
// ancestor's remote.
|
|
258
|
+
if (!source.repo && existsSync(join(repoPath, '.git'))) {
|
|
251
259
|
try {
|
|
252
260
|
const remote = execSync('git remote get-url origin 2>/dev/null', {
|
|
253
261
|
cwd: repoPath,
|
|
@@ -882,6 +890,60 @@ function verifyOcConfig(pluginDirName) {
|
|
|
882
890
|
}
|
|
883
891
|
}
|
|
884
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Phase 3b: remove a stale MCP registration for an extension whose current
|
|
895
|
+
* source no longer exposes an MCP interface. Keyed on path, not source
|
|
896
|
+
* metadata (buildSourceInfo is known to capture the parent repo's remote
|
|
897
|
+
* when extraction lands inside another git working tree).
|
|
898
|
+
*
|
|
899
|
+
* Removes the entry from ~/.claude.json#mcpServers if the args path
|
|
900
|
+
* resolves under LDM_EXTENSIONS/<toolName> or OC_EXTENSIONS/<toolName>.
|
|
901
|
+
* No-op if no entry exists or the path points elsewhere.
|
|
902
|
+
*/
|
|
903
|
+
function unregisterStaleMCP(toolName) {
|
|
904
|
+
const ccUserPath = join(HOME, '.claude.json');
|
|
905
|
+
const ccUser = readJSON(ccUserPath);
|
|
906
|
+
const entry = ccUser?.mcpServers?.[toolName];
|
|
907
|
+
if (!entry) return;
|
|
908
|
+
|
|
909
|
+
const firstArg = Array.isArray(entry.args) ? entry.args[0] : null;
|
|
910
|
+
if (!firstArg || typeof firstArg !== 'string') return;
|
|
911
|
+
|
|
912
|
+
const ldmExt = join(LDM_EXTENSIONS, toolName);
|
|
913
|
+
const ocExt = join(OC_EXTENSIONS, toolName);
|
|
914
|
+
const pointsHere = firstArg.startsWith(ldmExt + '/') || firstArg.startsWith(ocExt + '/');
|
|
915
|
+
if (!pointsHere) return;
|
|
916
|
+
|
|
917
|
+
if (DRY_RUN) {
|
|
918
|
+
ok(`MCP: would unregister stale ${toolName} entry pointing at ${firstArg} (dry run)`);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
execSync(`claude mcp remove ${toolName} --scope user`, { stdio: 'pipe' });
|
|
924
|
+
ok(`MCP: unregistered stale ${toolName} entry (source no longer exposes MCP)`);
|
|
925
|
+
} catch {
|
|
926
|
+
// Fallback: direct edit to ~/.claude.json
|
|
927
|
+
try {
|
|
928
|
+
const cfg = readJSON(ccUserPath) || {};
|
|
929
|
+
if (cfg.mcpServers && cfg.mcpServers[toolName]) {
|
|
930
|
+
delete cfg.mcpServers[toolName];
|
|
931
|
+
writeJSON(ccUserPath, cfg);
|
|
932
|
+
ok(`MCP: unregistered stale ${toolName} entry via direct .claude.json edit`);
|
|
933
|
+
}
|
|
934
|
+
} catch (e) {
|
|
935
|
+
log(`Warning: could not unregister stale MCP ${toolName}: ${e.message}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Also clean OpenClaw side if installed.
|
|
940
|
+
try {
|
|
941
|
+
execSync(`openclaw mcp unset ${toolName}`, { stdio: 'pipe' });
|
|
942
|
+
} catch {
|
|
943
|
+
// Non-fatal. OpenClaw may not be installed, or may not have had this mcp.
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
885
947
|
function registerMCP(repoPath, door, toolName) {
|
|
886
948
|
let rawName = toolName || door.name || basename(repoPath);
|
|
887
949
|
// Strip /tmp/ clone prefixes (ldm-install-, wip-install-)
|
|
@@ -894,6 +956,27 @@ function registerMCP(repoPath, door, toolName) {
|
|
|
894
956
|
: existsSync(ldmFallbackPath) ? ldmFallbackPath
|
|
895
957
|
: repoServerPath;
|
|
896
958
|
|
|
959
|
+
// Postcondition: the resolved entrypoint must exist and parse before we
|
|
960
|
+
// touch ~/.claude.json. Previously, if the published tarball did not
|
|
961
|
+
// contain the declared mcp-server file (see wip-1password 0.2.3-alpha.2
|
|
962
|
+
// bug writeup), we still wrote the registration and left a dangling
|
|
963
|
+
// "Failed to connect" entry that was invisible until the user ran
|
|
964
|
+
// `claude mcp list`. Fail loudly instead.
|
|
965
|
+
if (!existsSync(mcpPath)) {
|
|
966
|
+
fail(`MCP: ${name} registration aborted. Resolved path does not exist: ${mcpPath}`);
|
|
967
|
+
fail(`MCP: candidates checked: ${ldmServerPath}, ${ldmFallbackPath}, ${repoServerPath}`);
|
|
968
|
+
fail(`MCP: verify the published package includes "${door.file}" (check files array).`);
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
execSync(`node --check "${mcpPath}"`, { stdio: 'pipe', timeout: 5000 });
|
|
973
|
+
} catch (e) {
|
|
974
|
+
const stderr = (e && e.stderr && e.stderr.toString && e.stderr.toString().trim()) || (e && e.message) || 'unknown error';
|
|
975
|
+
fail(`MCP: ${name} registration aborted. Entrypoint failed node --check: ${mcpPath}`);
|
|
976
|
+
fail(`MCP: ${stderr}`);
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
|
|
897
980
|
// Check ~/.claude.json (user-level MCP)
|
|
898
981
|
const ccUserPath = join(HOME, '.claude.json');
|
|
899
982
|
const ccUser = readJSON(ccUserPath);
|
|
@@ -1272,6 +1355,12 @@ export function installSingleTool(toolPath) {
|
|
|
1272
1355
|
} else {
|
|
1273
1356
|
skip(`MCP: ${toolName} not enabled. Run: ldm enable ${toolName}`);
|
|
1274
1357
|
}
|
|
1358
|
+
} else {
|
|
1359
|
+
// Phase 3b: source no longer exposes an MCP interface (file renamed,
|
|
1360
|
+
// moved to src/, removed, etc). If a prior install registered an MCP
|
|
1361
|
+
// whose args point into this extension's directory, un-register it so
|
|
1362
|
+
// claude mcp list does not keep a dangling "Failed to connect" entry.
|
|
1363
|
+
unregisterStaleMCP(toolName);
|
|
1275
1364
|
}
|
|
1276
1365
|
|
|
1277
1366
|
if (interfaces.claudeCodeHook) {
|