@wipcomputer/universal-installer
Advanced tools
| # wip-universal-installer: refresh SKILL, REFERENCE, SPEC stub for eight interfaces + install-spec URL | ||
| Sibling PR to `wip-ldm-os-private#715`. Canonical docs there now describe the eight Universal Interfaces in canonical order, with Remote MCP at #4 and Claude Code Plugin at #8, plus the install-spec URL convention. This PR brings the toolbox tool docs into the same shape so they stop telling a different story. | ||
| ## What changed | ||
| - **`tools/wip-universal-installer/SKILL.md`** ... rewrite. `ldm install` is the primary command (was `wip-install`). `wip-install` is the standalone fallback. Adds the install-spec URL section, the eight interfaces in canonical order, and the pinned Remote MCP contract. `metadata.capabilities` updated: `install-mcp` split into `install-mcp-local` + `install-mcp-remote`; `install-claude-code-plugin` added. | ||
| - **`tools/wip-universal-installer/REFERENCE.md`** ... interface table now eight rows numbered, with local/remote MCP labeled. Detection table gains the Remote MCP and CC Plugin rows. The Installer section adds track flags and an Install Spec URL note. Pointer to canonical SPEC at the top. | ||
| - **`tools/wip-universal-installer/SPEC.md`** ... trimmed to a thin stub. Keeps the MOVED notice (already present), adds a one-screen summary (eight interfaces, Remote MCP contract, install-spec URL), and links to the canonical doc + master plan. Stops being a competing source of truth. | ||
| - **`README.md`** (toolbox root) ... one line updated: "all six interfaces" → "all eight interfaces" with the canonical list. | ||
| ## Why | ||
| The toolbox copies were the original canonical home and were marked MOVED in March 2026. The MOVED notice was at the top, but the body still described six interfaces in detail. After this PR, the body matches the canonical source (or defers to it cleanly), so an agent landing on either source gets the same picture. | ||
| ## Test plan | ||
| - [ ] Read `tools/wip-universal-installer/SKILL.md`. Eight interfaces in canonical order; ldm install primary; install spec URL section present; Remote MCP contract pinned. | ||
| - [ ] Read `tools/wip-universal-installer/REFERENCE.md`. Eight-row interface table; Remote MCP + CC Plugin in detection table; Installer section names tracks. | ||
| - [ ] Read `tools/wip-universal-installer/SPEC.md`. Stub only; no detailed interface sections; clear pointer to canonical. | ||
| - [ ] Read root `README.md`. "All eight interfaces" line present. |
+19
-1
@@ -7,3 +7,3 @@ /** | ||
| import { existsSync, readFileSync } from 'node:fs'; | ||
| import { existsSync, readFileSync, readdirSync } from 'node:fs'; | ||
| import { join, basename } from 'node:path'; | ||
@@ -97,2 +97,20 @@ | ||
| /** | ||
| * Detect if a repo is a toolbox (has tools/ subdirectories with package.json). | ||
| * Returns array of { name, path } for each sub-tool, or empty array if not a toolbox. | ||
| */ | ||
| export function detectToolbox(repoPath) { | ||
| const toolsDir = join(repoPath, 'tools'); | ||
| if (!existsSync(toolsDir)) return []; | ||
| try { | ||
| const entries = readdirSync(toolsDir, { withFileTypes: true }); | ||
| return entries | ||
| .filter(e => e.isDirectory() && existsSync(join(toolsDir, e.name, 'package.json'))) | ||
| .map(e => ({ name: e.name, path: join(toolsDir, e.name) })); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| /** | ||
| * Detect interfaces and return a structured JSON-serializable result. | ||
@@ -99,0 +117,0 @@ */ |
+756
-106
@@ -5,10 +5,18 @@ #!/usr/bin/env node | ||
| // Reads a repo, detects available interfaces, installs them all. | ||
| // Deploys to LDM OS (~/.ldm/extensions/) and OpenClaw (~/.openclaw/extensions/). | ||
| // Registers MCP servers at user scope via `claude mcp add --scope user`. | ||
| // Maintains a registry at ~/.ldm/extensions/registry.json. | ||
| import { execSync } from 'node:child_process'; | ||
| import { existsSync, readFileSync, writeFileSync, cpSync, mkdirSync } from 'node:fs'; | ||
| import { existsSync, readFileSync, writeFileSync, cpSync, mkdirSync, lstatSync, readlinkSync, unlinkSync, chmodSync, readdirSync } from 'node:fs'; | ||
| import { join, basename, resolve } from 'node:path'; | ||
| import { detectInterfaces, describeInterfaces, detectInterfacesJSON } from './detect.mjs'; | ||
| import { detectInterfaces, describeInterfaces, detectInterfacesJSON, detectToolbox } from './detect.mjs'; | ||
| const OPENCLAW_DIR = join(process.env.HOME, '.openclaw'); | ||
| const EXTENSIONS_DIR = join(OPENCLAW_DIR, 'extensions'); | ||
| const HOME = process.env.HOME || ''; | ||
| const LDM_ROOT = join(HOME, '.ldm'); | ||
| const LDM_EXTENSIONS = join(LDM_ROOT, 'extensions'); | ||
| const OC_ROOT = join(HOME, '.openclaw'); | ||
| const OC_EXTENSIONS = join(OC_ROOT, 'extensions'); | ||
| const OC_MCP = join(OC_ROOT, '.mcp.json'); | ||
| const REGISTRY_PATH = join(LDM_EXTENSIONS, 'registry.json'); | ||
@@ -26,2 +34,12 @@ // Flags | ||
| function ensureBinExecutable(binNames) { | ||
| try { | ||
| const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim(); | ||
| for (const bin of binNames) { | ||
| const binPath = join(npmPrefix, 'bin', bin); | ||
| try { chmodSync(binPath, 0o755); } catch {} | ||
| } | ||
| } catch {} | ||
| } | ||
| function readJSON(path) { | ||
@@ -35,15 +53,228 @@ try { | ||
| function writeJSON(path, data) { | ||
| mkdirSync(join(path, '..'), { recursive: true }); | ||
| writeFileSync(path, JSON.stringify(data, null, 2) + '\n'); | ||
| } | ||
| // ── Registry ── | ||
| function loadRegistry() { | ||
| return readJSON(REGISTRY_PATH) || { _format: 'v1', extensions: {} }; | ||
| } | ||
| function saveRegistry(registry) { | ||
| writeJSON(REGISTRY_PATH, registry); | ||
| } | ||
| function updateRegistry(name, info) { | ||
| const registry = loadRegistry(); | ||
| registry.extensions[name] = { | ||
| ...registry.extensions[name], | ||
| ...info, | ||
| updatedAt: new Date().toISOString(), | ||
| }; | ||
| saveRegistry(registry); | ||
| } | ||
| // ── Migration detection ── | ||
| /** | ||
| * Scan existing extension directories for installs that match this tool | ||
| * but live under a different directory name. Matches on: | ||
| * 1. Same package name in package.json | ||
| * 2. Same plugin id in openclaw.plugin.json | ||
| * Returns array of { dirName, matchType, path } for each match. | ||
| */ | ||
| function findExistingInstalls(toolName, pkg, ocPluginConfig) { | ||
| const matches = []; | ||
| const packageName = pkg?.name; | ||
| const pluginId = ocPluginConfig?.id; | ||
| // Scan both LDM and OC extension dirs | ||
| for (const extDir of [LDM_EXTENSIONS, OC_EXTENSIONS]) { | ||
| if (!existsSync(extDir)) continue; | ||
| let entries; | ||
| try { | ||
| entries = readdirSync(extDir, { withFileTypes: true }); | ||
| } catch { continue; } | ||
| for (const entry of entries) { | ||
| if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; | ||
| const dirName = entry.name; | ||
| // Skip if it's already the target name | ||
| if (dirName === toolName) continue; | ||
| // Skip registry.json | ||
| if (dirName === 'registry.json') continue; | ||
| const dirPath = join(extDir, dirName); | ||
| // Check package.json name match | ||
| if (packageName) { | ||
| const dirPkg = readJSON(join(dirPath, 'package.json')); | ||
| if (dirPkg?.name === packageName) { | ||
| if (!matches.some(m => m.dirName === dirName)) { | ||
| matches.push({ dirName, matchType: 'package', path: dirPath }); | ||
| } | ||
| continue; | ||
| } | ||
| } | ||
| // Check openclaw.plugin.json id match | ||
| if (pluginId) { | ||
| const dirPlugin = readJSON(join(dirPath, 'openclaw.plugin.json')); | ||
| if (dirPlugin?.id === pluginId) { | ||
| if (!matches.some(m => m.dirName === dirName)) { | ||
| matches.push({ dirName, matchType: 'plugin-id', path: dirPath }); | ||
| } | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return matches; | ||
| } | ||
| /** | ||
| * Migrate an existing install from an old directory name to the new one. | ||
| * Removes old extension dirs (LDM + OC), old MCP registrations, old skills. | ||
| */ | ||
| function migrateExistingInstall(oldName, newName) { | ||
| // 1. Remove old extension directories (check lstat too for broken symlinks) | ||
| for (const extDir of [LDM_EXTENSIONS, OC_EXTENSIONS]) { | ||
| const oldPath = join(extDir, oldName); | ||
| let pathExists = existsSync(oldPath); | ||
| if (!pathExists) { | ||
| try { lstatSync(oldPath); pathExists = true; } catch {} | ||
| } | ||
| if (pathExists) { | ||
| try { | ||
| execSync(`rm -rf "${oldPath}"`, { stdio: 'pipe' }); | ||
| ok(`Migrated: removed ${oldPath}`); | ||
| } catch (e) { | ||
| fail(`Migration: could not remove ${oldPath}. ${e.message}`); | ||
| } | ||
| } | ||
| } | ||
| // 2. Remove old MCP registrations (Claude Code) | ||
| try { | ||
| execSync(`claude mcp remove ${oldName} --scope user`, { stdio: 'pipe' }); | ||
| ok(`Migrated: removed MCP registration "${oldName}" from Claude Code`); | ||
| } catch {} | ||
| // Also clean ~/.claude/.mcp.json fallback | ||
| const ccMcpPath = join(HOME, '.claude', '.mcp.json'); | ||
| const ccMcp = readJSON(ccMcpPath); | ||
| if (ccMcp?.mcpServers?.[oldName]) { | ||
| delete ccMcp.mcpServers[oldName]; | ||
| writeJSON(ccMcpPath, ccMcp); | ||
| } | ||
| // Also clean ~/.claude.json (user-level MCP) | ||
| const ccUserPath = join(HOME, '.claude.json'); | ||
| const ccUser = readJSON(ccUserPath); | ||
| if (ccUser?.mcpServers?.[oldName]) { | ||
| delete ccUser.mcpServers[oldName]; | ||
| writeJSON(ccUserPath, ccUser); | ||
| ok(`Migrated: removed MCP registration "${oldName}" from ~/.claude.json`); | ||
| } | ||
| // 3. Remove old OpenClaw MCP registration | ||
| if (existsSync(OC_MCP)) { | ||
| const ocMcp = readJSON(OC_MCP); | ||
| if (ocMcp?.mcpServers?.[oldName]) { | ||
| delete ocMcp.mcpServers[oldName]; | ||
| writeJSON(OC_MCP, ocMcp); | ||
| ok(`Migrated: removed MCP registration "${oldName}" from OpenClaw`); | ||
| } | ||
| } | ||
| // 4. Remove old skill directory | ||
| const oldSkillDir = join(OC_ROOT, 'skills', oldName); | ||
| if (existsSync(oldSkillDir)) { | ||
| try { | ||
| execSync(`rm -rf "${oldSkillDir}"`, { stdio: 'pipe' }); | ||
| ok(`Migrated: removed old skill at ${oldSkillDir}`); | ||
| } catch {} | ||
| } | ||
| // 5. Remove old registry entry | ||
| const registry = loadRegistry(); | ||
| if (registry.extensions[oldName]) { | ||
| delete registry.extensions[oldName]; | ||
| saveRegistry(registry); | ||
| } | ||
| } | ||
| // ── Install functions ── | ||
| function installCLI(repoPath, door) { | ||
| const pkg = readJSON(join(repoPath, 'package.json')); | ||
| const binNames = typeof door.bin === 'string' ? [basename(repoPath)] : Object.keys(door.bin || {}); | ||
| const newVersion = pkg?.version; | ||
| // Check if already installed at this version | ||
| if (newVersion && binNames.length > 0) { | ||
| try { | ||
| const installed = execSync(`npm list -g ${pkg.name} --json 2>/dev/null`, { encoding: 'utf8' }); | ||
| const data = JSON.parse(installed); | ||
| const deps = data.dependencies || {}; | ||
| if (deps[pkg.name]?.version === newVersion) { | ||
| // Still ensure bins are executable (git doesn't preserve +x) | ||
| ensureBinExecutable(binNames); | ||
| skip(`CLI: ${binNames.join(', ')} already at v${newVersion}`); | ||
| return true; | ||
| } | ||
| } catch {} | ||
| } | ||
| if (DRY_RUN) { | ||
| ok(`CLI: would install globally (dry run)`); | ||
| ok(`CLI: would install ${binNames.join(', ')} globally (dry run)`); | ||
| return true; | ||
| } | ||
| // If the package has a build script and dist/ is missing, build first | ||
| if (pkg?.scripts?.build && !existsSync(join(repoPath, 'dist'))) { | ||
| try { | ||
| log(`CLI: building ${binNames.join(', ')} (TypeScript)...`); | ||
| execSync('npm run build', { cwd: repoPath, stdio: 'pipe' }); | ||
| } catch (e) { | ||
| fail(`CLI: build failed. ${e.stderr?.toString()?.slice(0, 200) || e.message}`); | ||
| } | ||
| } | ||
| try { | ||
| execSync('npm install -g .', { cwd: repoPath, stdio: 'pipe' }); | ||
| const binNames = typeof door.bin === 'string' ? [basename(repoPath)] : Object.keys(door.bin); | ||
| // Safety net: ensure bin files are executable (git doesn't always preserve +x) | ||
| ensureBinExecutable(binNames); | ||
| ok(`CLI: ${binNames.join(', ')} installed globally`); | ||
| return true; | ||
| } catch (e) { | ||
| const stderr = e.stderr?.toString() || ''; | ||
| // EEXIST: a binary with the same name exists from a different package. | ||
| // Remove the stale symlink and retry. | ||
| if (stderr.includes('EEXIST')) { | ||
| for (const bin of binNames) { | ||
| try { | ||
| const binPath = execSync(`npm config get prefix`, { encoding: 'utf8' }).trim() + '/bin/' + bin; | ||
| if (existsSync(binPath) && lstatSync(binPath).isSymbolicLink()) { | ||
| const target = readlinkSync(binPath); | ||
| // Only remove if it points to a different package | ||
| if (!target.includes(pkg.name.replace(/^@[^/]+\//, ''))) { | ||
| unlinkSync(binPath); | ||
| } | ||
| } | ||
| } catch {} | ||
| } | ||
| try { | ||
| execSync('npm install -g .', { cwd: repoPath, stdio: 'pipe' }); | ||
| ensureBinExecutable(binNames); | ||
| ok(`CLI: ${binNames.join(', ')} installed globally (replaced stale symlink)`); | ||
| return true; | ||
| } catch {} | ||
| } | ||
| try { | ||
| execSync('npm link', { cwd: repoPath, stdio: 'pipe' }); | ||
| ensureBinExecutable(binNames); | ||
| ok(`CLI: linked globally via npm link`); | ||
@@ -58,13 +289,33 @@ return true; | ||
| function installOpenClaw(repoPath, door) { | ||
| const name = door.config?.name || basename(repoPath); | ||
| const dest = join(EXTENSIONS_DIR, name); | ||
| function deployExtension(repoPath, name) { | ||
| const ldmDest = join(LDM_EXTENSIONS, name); | ||
| const ocDest = join(OC_EXTENSIONS, name); | ||
| if (DRY_RUN) { | ||
| ok(`OpenClaw: would copy to ${dest} (dry run)`); | ||
| // Check if already deployed at the same version | ||
| const sourcePkg = readJSON(join(repoPath, 'package.json')); | ||
| const installedPkg = readJSON(join(ldmDest, 'package.json')); | ||
| const newVersion = sourcePkg?.version; | ||
| const currentVersion = installedPkg?.version; | ||
| if (newVersion && currentVersion && newVersion === currentVersion) { | ||
| skip(`LDM: ${name} already at v${currentVersion}`); | ||
| // Still check OpenClaw copy exists | ||
| if (existsSync(ocDest)) { | ||
| skip(`OpenClaw: ${name} already at v${currentVersion}`); | ||
| } else if (!DRY_RUN) { | ||
| // LDM has it but OpenClaw doesn't. Copy it over. | ||
| mkdirSync(ocDest, { recursive: true }); | ||
| cpSync(ldmDest, ocDest, { recursive: true }); | ||
| ok(`OpenClaw: deployed to ${ocDest} (synced from LDM)`); | ||
| } | ||
| return true; | ||
| } | ||
| if (existsSync(dest)) { | ||
| skip(`OpenClaw: ${name} already installed at ${dest}`); | ||
| if (DRY_RUN) { | ||
| if (currentVersion) { | ||
| ok(`LDM: would upgrade ${name} v${currentVersion} -> v${newVersion} (dry run)`); | ||
| } else { | ||
| ok(`LDM: would deploy ${name} v${newVersion || 'unknown'} to ${ldmDest} (dry run)`); | ||
| } | ||
| ok(`OpenClaw: would deploy to ${ocDest} (dry run)`); | ||
| return true; | ||
@@ -74,17 +325,38 @@ } | ||
| try { | ||
| mkdirSync(dest, { recursive: true }); | ||
| cpSync(repoPath, dest, { recursive: true, filter: (src) => !src.includes('.git') }); | ||
| ok(`OpenClaw: copied to ${dest}`); | ||
| // LDM path (remove existing to get clean copy) | ||
| if (existsSync(ldmDest)) { | ||
| execSync(`rm -rf "${ldmDest}"`, { stdio: 'pipe' }); | ||
| } | ||
| mkdirSync(ldmDest, { recursive: true }); | ||
| cpSync(repoPath, ldmDest, { | ||
| recursive: true, | ||
| filter: (src) => !src.includes('.git') && !src.includes('node_modules') && !src.includes('ai/') | ||
| }); | ||
| if (currentVersion) { | ||
| ok(`LDM: upgraded ${name} v${currentVersion} -> v${newVersion}`); | ||
| } else { | ||
| ok(`LDM: deployed to ${ldmDest}`); | ||
| } | ||
| if (existsSync(join(dest, 'package.json'))) { | ||
| // Install deps in LDM | ||
| if (existsSync(join(ldmDest, 'package.json'))) { | ||
| try { | ||
| execSync('npm install --omit=dev', { cwd: dest, stdio: 'pipe' }); | ||
| ok(`OpenClaw: dependencies installed`); | ||
| execSync('npm install --omit=dev', { cwd: ldmDest, stdio: 'pipe' }); | ||
| ok(`LDM: dependencies installed`); | ||
| } catch { | ||
| skip(`OpenClaw: no deps needed`); | ||
| skip(`LDM: no deps needed`); | ||
| } | ||
| } | ||
| // OpenClaw path (copy from LDM to keep them identical) | ||
| if (existsSync(ocDest)) { | ||
| execSync(`rm -rf "${ocDest}"`, { stdio: 'pipe' }); | ||
| } | ||
| mkdirSync(ocDest, { recursive: true }); | ||
| cpSync(ldmDest, ocDest, { recursive: true }); | ||
| ok(`OpenClaw: deployed to ${ocDest}`); | ||
| return true; | ||
| } catch (e) { | ||
| fail(`OpenClaw: copy failed. ${e.message}`); | ||
| fail(`Deploy failed: ${e.message}`); | ||
| return false; | ||
@@ -94,4 +366,101 @@ } | ||
| function installOpenClaw(repoPath, door, toolName) { | ||
| // Use toolName (from package.json name, stripped of scope) for the directory. | ||
| // Never use door.config.name (display name) ... it can have spaces. | ||
| const name = toolName || door.config?.id || basename(repoPath); | ||
| return deployExtension(repoPath, name); | ||
| } | ||
| function registerMCP(repoPath, door, toolName) { | ||
| // Use toolName for the MCP registration name and LDM path lookup. | ||
| // Strip npm scope (@org/) from name for claude mcp add compatibility. | ||
| const rawName = toolName || door.name || basename(repoPath); | ||
| const name = rawName.replace(/^@[\w-]+\//, ''); | ||
| const serverPath = join(repoPath, door.file); | ||
| // Use LDM-deployed path if it exists, otherwise repo path. | ||
| // Try toolName first (correct), then basename(repoPath) as fallback. | ||
| const ldmServerPath = join(LDM_EXTENSIONS, name, door.file); | ||
| const ldmFallbackPath = join(LDM_EXTENSIONS, basename(repoPath), door.file); | ||
| const mcpPath = existsSync(ldmServerPath) ? ldmServerPath | ||
| : existsSync(ldmFallbackPath) ? ldmFallbackPath | ||
| : serverPath; | ||
| // Check if already registered with the same path | ||
| const ccMcpPath = join(HOME, '.claude', '.mcp.json'); | ||
| const ccMcp = readJSON(ccMcpPath); | ||
| const ccAlreadyRegistered = ccMcp?.mcpServers?.[name]?.args?.includes(mcpPath); | ||
| let ocAlreadyRegistered = false; | ||
| if (existsSync(OC_MCP)) { | ||
| const ocMcp = readJSON(OC_MCP); | ||
| ocAlreadyRegistered = ocMcp?.mcpServers?.[name]?.args?.includes(mcpPath); | ||
| } | ||
| if (ccAlreadyRegistered && (ocAlreadyRegistered || !existsSync(OC_MCP))) { | ||
| skip(`MCP: ${name} already registered at ${mcpPath}`); | ||
| return true; | ||
| } | ||
| if (DRY_RUN) { | ||
| if (!ccAlreadyRegistered) ok(`MCP (CC): would register ${name} at user scope (dry run)`); | ||
| if (!ocAlreadyRegistered && existsSync(OC_MCP)) ok(`MCP (OC): would add to ${OC_MCP} (dry run)`); | ||
| return true; | ||
| } | ||
| // 1. Register with Claude Code CLI at user scope | ||
| let ccRegistered = ccAlreadyRegistered; | ||
| if (!ccAlreadyRegistered) { | ||
| try { | ||
| // Remove first if exists (update behavior) | ||
| try { | ||
| execSync(`claude mcp remove ${name} --scope user`, { stdio: 'pipe' }); | ||
| } catch {} | ||
| const envFlag = existsSync(OC_ROOT) ? ` -e OPENCLAW_HOME="${OC_ROOT}"` : ''; | ||
| execSync(`claude mcp add --scope user ${name}${envFlag} -- node "${mcpPath}"`, { stdio: 'pipe' }); | ||
| ok(`MCP (CC): registered ${name} at user scope`); | ||
| ccRegistered = true; | ||
| } catch (e) { | ||
| // Fallback: write to ~/.claude/.mcp.json | ||
| try { | ||
| const mcpConfig = readJSON(ccMcpPath) || { mcpServers: {} }; | ||
| mcpConfig.mcpServers[name] = { | ||
| command: 'node', | ||
| args: [mcpPath], | ||
| }; | ||
| writeJSON(ccMcpPath, mcpConfig); | ||
| ok(`MCP (CC): registered ${name} in ~/.claude/.mcp.json (fallback)`); | ||
| ccRegistered = true; | ||
| } catch (e2) { | ||
| fail(`MCP (CC): registration failed. ${e.message}`); | ||
| } | ||
| } | ||
| } else { | ||
| skip(`MCP (CC): ${name} already registered`); | ||
| } | ||
| // 2. Register in OpenClaw's .mcp.json (only if the file already exists) | ||
| if (existsSync(OC_MCP) && !ocAlreadyRegistered) { | ||
| try { | ||
| const ocMcp = readJSON(OC_MCP) || { mcpServers: {} }; | ||
| ocMcp.mcpServers[name] = { | ||
| command: 'node', | ||
| args: [mcpPath], | ||
| }; | ||
| if (existsSync(OC_ROOT)) { | ||
| ocMcp.mcpServers[name].env = { OPENCLAW_HOME: OC_ROOT }; | ||
| } | ||
| writeJSON(OC_MCP, ocMcp); | ||
| ok(`MCP (OC): registered ${name} in ${OC_MCP}`); | ||
| } catch (e) { | ||
| fail(`MCP (OC): registration failed. ${e.message}`); | ||
| } | ||
| } else if (existsSync(OC_MCP) && ocAlreadyRegistered) { | ||
| skip(`MCP (OC): ${name} already registered`); | ||
| } | ||
| return ccRegistered; | ||
| } | ||
| function installClaudeCodeHook(repoPath, door) { | ||
| const settingsPath = join(process.env.HOME, '.claude', 'settings.json'); | ||
| const settingsPath = join(HOME, '.claude', 'settings.json'); | ||
| let settings = readJSON(settingsPath); | ||
@@ -104,2 +473,9 @@ | ||
| // Always use the installed extension path, never repo clones or /tmp/ | ||
| const toolName = basename(repoPath); | ||
| const installedGuard = join(LDM_EXTENSIONS, toolName, 'guard.mjs'); | ||
| const hookCommand = existsSync(installedGuard) | ||
| ? `node ${installedGuard}` | ||
| : (door.command || `node "${join(repoPath, 'guard.mjs')}"`); | ||
| if (DRY_RUN) { | ||
@@ -115,10 +491,29 @@ ok(`Claude Code: would add ${door.event || 'PreToolUse'} hook (dry run)`); | ||
| const hookCommand = door.command || `node "${join(repoPath, 'guard.mjs')}"`; | ||
| const existing = settings.hooks[event].some(entry => | ||
| entry.hooks?.some(h => h.command === hookCommand) | ||
| // Match by tool name in the command path, not exact string. | ||
| // This prevents duplicates when the same tool is installed from different paths. | ||
| const guardFile = basename(door.command || 'guard.mjs').replace(/^node\s+/, '').replace(/"/g, ''); | ||
| const existingIdx = settings.hooks[event].findIndex(entry => | ||
| entry.hooks?.some(h => { | ||
| const cmd = h.command || ''; | ||
| return cmd.includes(`/${toolName}/`) || cmd === hookCommand; | ||
| }) | ||
| ); | ||
| if (existing) { | ||
| skip(`Claude Code: ${event} hook already configured`); | ||
| return true; | ||
| if (existingIdx !== -1) { | ||
| const existingCmd = settings.hooks[event][existingIdx].hooks?.[0]?.command || ''; | ||
| if (existingCmd === hookCommand) { | ||
| skip(`Claude Code: ${event} hook already configured`); | ||
| return true; | ||
| } | ||
| // Update the existing hook to point to the installed location | ||
| settings.hooks[event][existingIdx].hooks[0].command = hookCommand; | ||
| settings.hooks[event][existingIdx].hooks[0].timeout = door.timeout || 10; | ||
| try { | ||
| writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); | ||
| ok(`Claude Code: ${event} hook updated to installed path`); | ||
| return true; | ||
| } catch (e) { | ||
| fail(`Claude Code: failed to update settings.json. ${e.message}`); | ||
| return false; | ||
| } | ||
| } | ||
@@ -145,2 +540,176 @@ | ||
| function installSkill(repoPath, toolName) { | ||
| const skillSrc = join(repoPath, 'SKILL.md'); | ||
| const ocSkillDir = join(OC_ROOT, 'skills', toolName); | ||
| const ocSkillDest = join(ocSkillDir, 'SKILL.md'); | ||
| // Check if already deployed with same content | ||
| if (existsSync(ocSkillDest)) { | ||
| try { | ||
| const srcContent = readFileSync(skillSrc, 'utf8'); | ||
| const destContent = readFileSync(ocSkillDest, 'utf8'); | ||
| if (srcContent === destContent) { | ||
| skip(`Skill: ${toolName} already deployed to OpenClaw`); | ||
| return true; | ||
| } | ||
| } catch {} | ||
| } | ||
| if (DRY_RUN) { | ||
| ok(`Skill: would deploy ${toolName}/SKILL.md to ${ocSkillDir} (dry run)`); | ||
| return true; | ||
| } | ||
| try { | ||
| mkdirSync(ocSkillDir, { recursive: true }); | ||
| cpSync(skillSrc, ocSkillDest); | ||
| ok(`Skill: deployed to ${ocSkillDir}`); | ||
| return true; | ||
| } catch (e) { | ||
| fail(`Skill: deploy failed. ${e.message}`); | ||
| return false; | ||
| } | ||
| } | ||
| // ── Worktree gitignore ── | ||
| function ensureWorktreeGitignore(repoPath) { | ||
| // Only for local repos, not /tmp/ clones | ||
| if (repoPath.startsWith('/tmp/')) return; | ||
| if (!existsSync(join(repoPath, '.git'))) return; | ||
| const gitignorePath = join(repoPath, '.gitignore'); | ||
| const entry = '.claude/worktrees/'; | ||
| if (existsSync(gitignorePath)) { | ||
| const content = readFileSync(gitignorePath, 'utf8'); | ||
| if (content.includes(entry)) return; // already present | ||
| if (DRY_RUN) { | ||
| ok(`Gitignore: would add ${entry} to ${gitignorePath} (dry run)`); | ||
| return; | ||
| } | ||
| const separator = content.endsWith('\n') ? '' : '\n'; | ||
| writeFileSync(gitignorePath, content + separator + entry + '\n'); | ||
| } else { | ||
| if (DRY_RUN) { | ||
| ok(`Gitignore: would create ${gitignorePath} with ${entry} (dry run)`); | ||
| return; | ||
| } | ||
| writeFileSync(gitignorePath, entry + '\n'); | ||
| } | ||
| ok(`Gitignore: added ${entry} to .gitignore`); | ||
| } | ||
| // ── Single tool install ── | ||
| function installSingleTool(toolPath) { | ||
| const { interfaces, pkg } = detectInterfaces(toolPath); | ||
| const ifaceNames = Object.keys(interfaces); | ||
| if (ifaceNames.length === 0) return 0; | ||
| const toolName = pkg?.name?.replace(/^@\w+\//, '') || basename(toolPath); | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| console.log(` Installing: ${toolName}${DRY_RUN ? ' (dry run)' : ''}`); | ||
| console.log(` ${'─'.repeat(40)}`); | ||
| log(`Detected ${ifaceNames.length} interface(s): ${ifaceNames.join(', ')}`); | ||
| console.log(''); | ||
| } | ||
| if (DRY_RUN && !JSON_OUTPUT) { | ||
| console.log(describeInterfaces(interfaces)); | ||
| // Show migration preview in dry run | ||
| const existing = findExistingInstalls(toolName, pkg, interfaces.openclaw?.config); | ||
| if (existing.length > 0) { | ||
| console.log(''); | ||
| for (const m of existing) { | ||
| log(`Migration: would rename "${m.dirName}" -> "${toolName}" (matched by ${m.matchType})`); | ||
| } | ||
| } | ||
| return ifaceNames.length; | ||
| } | ||
| // Detect and migrate existing installs under different names | ||
| const existing = findExistingInstalls(toolName, pkg, interfaces.openclaw?.config); | ||
| if (existing.length > 0) { | ||
| console.log(''); | ||
| const migrated = new Set(); | ||
| for (const m of existing) { | ||
| if (!migrated.has(m.dirName)) { | ||
| log(`Found existing install: "${m.dirName}" (matched by ${m.matchType}). Migrating to "${toolName}"...`); | ||
| migrateExistingInstall(m.dirName, toolName); | ||
| migrated.add(m.dirName); | ||
| } | ||
| } | ||
| console.log(''); | ||
| } | ||
| let installed = 0; | ||
| const registryInfo = { | ||
| name: toolName, | ||
| version: pkg?.version || 'unknown', | ||
| source: toolPath, | ||
| interfaces: ifaceNames, | ||
| }; | ||
| if (interfaces.cli) { | ||
| if (installCLI(toolPath, interfaces.cli)) installed++; | ||
| } | ||
| // Deploy to LDM + OpenClaw (for plugins or any extension with MCP) | ||
| if (interfaces.openclaw) { | ||
| if (installOpenClaw(toolPath, interfaces.openclaw, toolName)) { | ||
| installed++; | ||
| registryInfo.ldmPath = join(LDM_EXTENSIONS, toolName); | ||
| registryInfo.ocPath = join(OC_EXTENSIONS, toolName); | ||
| } | ||
| } else if (interfaces.mcp) { | ||
| // Even without openclaw.plugin.json, deploy to LDM for MCP server access | ||
| const extName = basename(toolPath); | ||
| if (deployExtension(toolPath, extName)) { | ||
| registryInfo.ldmPath = join(LDM_EXTENSIONS, extName); | ||
| registryInfo.ocPath = join(OC_EXTENSIONS, extName); | ||
| } | ||
| } | ||
| if (interfaces.mcp) { | ||
| if (registerMCP(toolPath, interfaces.mcp, toolName)) installed++; | ||
| } | ||
| if (interfaces.claudeCodeHook) { | ||
| if (installClaudeCodeHook(toolPath, interfaces.claudeCodeHook)) installed++; | ||
| } | ||
| if (interfaces.skill) { | ||
| if (installSkill(toolPath, toolName)) installed++; | ||
| } | ||
| if (interfaces.module) { | ||
| ok(`Module: import from "${interfaces.module.main}"`); | ||
| installed++; | ||
| } | ||
| // Update registry | ||
| if (!DRY_RUN) { | ||
| try { | ||
| mkdirSync(LDM_EXTENSIONS, { recursive: true }); | ||
| updateRegistry(toolName, registryInfo); | ||
| ok(`Registry: updated`); | ||
| } catch (e) { | ||
| fail(`Registry: update failed. ${e.message}`); | ||
| } | ||
| } | ||
| // Ensure .claude/worktrees/ is in the repo's .gitignore | ||
| ensureWorktreeGitignore(toolPath); | ||
| return installed; | ||
| } | ||
| // ── Main ── | ||
| async function main() { | ||
@@ -160,13 +729,96 @@ if (!target || target === '--help' || target === '-h') { | ||
| console.log(''); | ||
| console.log(' Interfaces it detects:'); | ||
| console.log(' Interfaces it detects and installs:'); | ||
| console.log(' CLI ... package.json bin entry -> npm install -g'); | ||
| console.log(' Module ... ESM main/exports -> importable'); | ||
| console.log(' MCP Server ... mcp-server.mjs -> config for .mcp.json'); | ||
| console.log(' OpenClaw ... openclaw.plugin.json -> copies to extensions/'); | ||
| console.log(' Skill ... SKILL.md -> agent instructions'); | ||
| console.log(' CC Hook ... guard.mjs or claudeCode.hook -> settings.json'); | ||
| console.log(' MCP Server ... mcp-server.mjs -> claude mcp add --scope user'); | ||
| console.log(' OpenClaw ... openclaw.plugin.json -> ~/.ldm/extensions/ + ~/.openclaw/extensions/'); | ||
| console.log(' Skill ... SKILL.md -> ~/.openclaw/skills/<tool>/'); | ||
| console.log(' CC Hook ... guard.mjs or claudeCode.hook -> ~/.claude/settings.json'); | ||
| console.log(''); | ||
| console.log(' Modes:'); | ||
| console.log(' Single repo ... installs one tool'); | ||
| console.log(' Toolbox ... detects tools/ subdirectories, installs each sub-tool'); | ||
| console.log(''); | ||
| console.log(' Paths:'); | ||
| console.log(' LDM: ~/.ldm/extensions/<name>/ (primary, for Claude Code)'); | ||
| console.log(' OpenClaw: ~/.openclaw/extensions/<name>/ (for Lesa/OpenClaw)'); | ||
| console.log(' Registry: ~/.ldm/extensions/registry.json'); | ||
| console.log(''); | ||
| process.exit(0); | ||
| } | ||
| // ── LDM bootstrap ── | ||
| // If ldm is not on PATH, try to install it silently before falling back. | ||
| function bootstrapLdmOs() { | ||
| try { | ||
| execSync('npm install -g @wipcomputer/wip-ldm-os', { stdio: 'pipe', timeout: 120000 }); | ||
| execSync('ldm --version', { stdio: 'pipe', timeout: 5000 }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| // ── LDM delegation ── | ||
| // If the ldm CLI is on PATH, delegate the entire install to it. | ||
| // ldm install understands all the same interfaces and adds LDM OS orchestration. | ||
| // Check early, before URL/path resolution, so we don't clone unnecessarily. | ||
| let ldmAvailable = false; | ||
| try { | ||
| execSync('ldm --version', { stdio: 'pipe' }); | ||
| ldmAvailable = true; | ||
| // ldm is available, delegate | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| console.log(' LDM OS detected. Delegating to ldm install...'); | ||
| console.log(''); | ||
| } | ||
| const flags = args.filter(a => a.startsWith('--')); | ||
| const rawTarget = process.argv[2]; | ||
| execSync(`ldm install ${rawTarget} ${flags.join(' ')}`, { stdio: 'inherit' }); | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| console.log(' Tip: Run "ldm install" to see more components you can add.'); | ||
| } | ||
| process.exit(0); | ||
| } catch (e) { | ||
| if (!ldmAvailable) { | ||
| // ldm not on PATH, try bootstrap | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| console.log(' Installing LDM OS infrastructure...'); | ||
| console.log(''); | ||
| } | ||
| if (bootstrapLdmOs()) { | ||
| ldmAvailable = true; | ||
| if (!JSON_OUTPUT) { | ||
| console.log(' LDM OS installed. Delegating to ldm install...'); | ||
| console.log(''); | ||
| } | ||
| // Now delegate | ||
| const flags = args.filter(a => a.startsWith('--')); | ||
| const rawTarget = process.argv[2]; | ||
| try { | ||
| execSync(`ldm install ${rawTarget} ${flags.join(' ')}`, { stdio: 'inherit' }); | ||
| process.exit(0); | ||
| } catch (delegateErr) { | ||
| if (!JSON_OUTPUT) console.error(' ldm install failed. Falling back to standalone installer.'); | ||
| } | ||
| } else { | ||
| if (!JSON_OUTPUT) { | ||
| console.log(' LDM OS install skipped (npm offline or permissions issue). Using standalone.'); | ||
| console.log(''); | ||
| } | ||
| } | ||
| } else { | ||
| // ldm exists but install command failed | ||
| if (!JSON_OUTPUT) console.error(' ldm install failed. Falling back to standalone installer.'); | ||
| } | ||
| } | ||
| // Resolve target: GitHub URL, org/repo shorthand, or local path | ||
@@ -176,10 +828,14 @@ let repoPath; | ||
| if (target.startsWith('http') || target.startsWith('git@') || target.match(/^[\w-]+\/[\w.-]+$/)) { | ||
| const url = target.match(/^[\w-]+\/[\w.-]+$/) | ||
| const isShorthand = target.match(/^[\w-]+\/[\w.-]+$/); | ||
| const httpsUrl = isShorthand | ||
| ? `https://github.com/${target}.git` | ||
| : target; | ||
| const repoName = basename(url).replace('.git', ''); | ||
| const sshUrl = isShorthand | ||
| ? `git@github.com:${target}.git` | ||
| : target.replace(/^https:\/\/github\.com\//, 'git@github.com:'); | ||
| const repoName = basename(httpsUrl).replace('.git', ''); | ||
| repoPath = join('/tmp', `wip-install-${repoName}`); | ||
| log(''); | ||
| log(`Cloning ${url}...`); | ||
| log(`Cloning ${httpsUrl}...`); | ||
| try { | ||
@@ -189,6 +845,13 @@ if (existsSync(repoPath)) { | ||
| } | ||
| execSync(`git clone "${url}" "${repoPath}"`, { stdio: 'pipe' }); | ||
| try { | ||
| execSync(`git clone "${httpsUrl}" "${repoPath}"`, { stdio: 'pipe' }); | ||
| } catch { | ||
| // HTTPS failed (private repo or no auth). Fall back to SSH. | ||
| log(`HTTPS clone failed. Trying SSH...`); | ||
| if (existsSync(repoPath)) execSync(`rm -rf "${repoPath}"`); | ||
| execSync(`git clone "${sshUrl}" "${repoPath}"`, { stdio: 'pipe' }); | ||
| } | ||
| ok(`Cloned to ${repoPath}`); | ||
| } catch (e) { | ||
| fail(`Clone failed: ${e.message}`); | ||
| fail(`Clone failed (tried HTTPS + SSH): ${e.message}`); | ||
| process.exit(1); | ||
@@ -204,84 +867,71 @@ } | ||
| // JSON mode: detect and output | ||
| if (JSON_OUTPUT) { | ||
| const result = detectInterfacesJSON(repoPath); | ||
| console.log(JSON.stringify(result, null, 2)); | ||
| if (DRY_RUN) process.exit(0); | ||
| // If not dry run, continue with install but suppress output | ||
| } | ||
| // Check for toolbox mode (tools/ subdirectories with package.json) | ||
| const subTools = detectToolbox(repoPath); | ||
| // Detect interfaces | ||
| const { interfaces, pkg } = detectInterfaces(repoPath); | ||
| const ifaceNames = Object.keys(interfaces); | ||
| if (subTools.length > 0) { | ||
| // Toolbox mode: install each sub-tool | ||
| const toolboxPkg = readJSON(join(repoPath, 'package.json')); | ||
| const toolboxName = toolboxPkg?.name?.replace(/^@\w+\//, '') || basename(repoPath); | ||
| if (ifaceNames.length === 0) { | ||
| skip('No installable interfaces detected.'); | ||
| process.exit(0); | ||
| } | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| console.log(` Toolbox: ${toolboxName}`); | ||
| console.log(` ${'═'.repeat(50)}`); | ||
| log(`Found ${subTools.length} sub-tool(s): ${subTools.map(t => t.name).join(', ')}`); | ||
| } | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| const repoName = basename(repoPath); | ||
| console.log(` Installing: ${repoName}${DRY_RUN ? ' (dry run)' : ''}`); | ||
| console.log(` ${'─'.repeat(40)}`); | ||
| log(`Detected ${ifaceNames.length} interface(s): ${ifaceNames.join(', ')}`); | ||
| console.log(''); | ||
| } | ||
| let totalInstalled = 0; | ||
| let toolsProcessed = 0; | ||
| if (DRY_RUN && !JSON_OUTPUT) { | ||
| // In dry run, show what would happen | ||
| console.log(describeInterfaces(interfaces)); | ||
| console.log(''); | ||
| console.log(' Dry run complete. No changes made.'); | ||
| console.log(''); | ||
| process.exit(0); | ||
| } | ||
| for (const subTool of subTools) { | ||
| const count = installSingleTool(subTool.path); | ||
| totalInstalled += count; | ||
| if (count > 0) toolsProcessed++; | ||
| } | ||
| // Install each interface | ||
| let installed = 0; | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| console.log(` ${'═'.repeat(50)}`); | ||
| if (DRY_RUN) { | ||
| console.log(` Dry run complete. ${toolsProcessed} tool(s) scanned, ${totalInstalled} interface(s) detected.`); | ||
| } else { | ||
| console.log(` Done. ${toolsProcessed} tool(s), ${totalInstalled} interface(s) processed.`); | ||
| } | ||
| console.log(''); | ||
| } | ||
| } else { | ||
| // Single repo mode | ||
| if (JSON_OUTPUT) { | ||
| const result = detectInterfacesJSON(repoPath); | ||
| console.log(JSON.stringify(result, null, 2)); | ||
| if (DRY_RUN) process.exit(0); | ||
| } | ||
| if (interfaces.cli) { | ||
| installCLI(repoPath, interfaces.cli); | ||
| installed++; | ||
| } | ||
| const installed = installSingleTool(repoPath); | ||
| if (interfaces.openclaw) { | ||
| installOpenClaw(repoPath, interfaces.openclaw); | ||
| installed++; | ||
| } | ||
| if (installed === 0) { | ||
| skip('No installable interfaces detected.'); | ||
| process.exit(0); | ||
| } | ||
| if (interfaces.claudeCodeHook) { | ||
| installClaudeCodeHook(repoPath, interfaces.claudeCodeHook); | ||
| installed++; | ||
| } | ||
| if (interfaces.mcp) { | ||
| if (!JSON_OUTPUT) { | ||
| console.log(''); | ||
| log(`MCP Server detected: ${interfaces.mcp.file}`); | ||
| log(`Add to .mcp.json:`); | ||
| console.log(JSON.stringify({ | ||
| [interfaces.mcp.name]: { | ||
| command: 'node', | ||
| args: [join(repoPath, interfaces.mcp.file)] | ||
| } | ||
| }, null, 2)); | ||
| if (DRY_RUN) { | ||
| console.log(' Dry run complete. No changes made.'); | ||
| } else { | ||
| console.log(` Done. ${installed} interface(s) processed.`); | ||
| } | ||
| console.log(''); | ||
| } | ||
| installed++; | ||
| } | ||
| if (interfaces.skill) { | ||
| ok(`Skill: SKILL.md available at ${interfaces.skill.path}`); | ||
| installed++; | ||
| } | ||
| if (interfaces.module) { | ||
| ok(`Module: import from "${interfaces.module.main}"`); | ||
| installed++; | ||
| } | ||
| if (!JSON_OUTPUT) { | ||
| // ── LDM OS tip (standalone install only) ── | ||
| // We only reach here if ldm was not available or ldm install failed. | ||
| if (!JSON_OUTPUT && !DRY_RUN) { | ||
| if (ldmAvailable) { | ||
| console.log(' Tip: Run "ldm install" to see more components you can add.'); | ||
| } else { | ||
| console.log(' Tip: LDM OS could not be installed automatically. Try: npm install -g @wipcomputer/wip-ldm-os'); | ||
| } | ||
| console.log(''); | ||
| console.log(` Done. ${installed} interface(s) processed.`); | ||
| console.log(''); | ||
| } | ||
@@ -288,0 +938,0 @@ } |
+33
-2
@@ -1,5 +0,9 @@ | ||
| MIT License | ||
| Dual License: MIT + AGPLv3 | ||
| Copyright (c) 2026 Parker Todd Brooks | ||
| Copyright (c) 2026 WIP Computer, Inc. | ||
| 1. MIT License (local and personal use) | ||
| --------------------------------------- | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
@@ -22,1 +26,28 @@ of this software and associated documentation files (the "Software"), to deal | ||
| SOFTWARE. | ||
| 2. GNU Affero General Public License v3.0 (commercial and cloud use) | ||
| -------------------------------------------------------------------- | ||
| If you run this software as part of a hosted service, cloud platform, | ||
| marketplace listing, or any network-accessible offering for commercial | ||
| purposes, the AGPLv3 terms apply. You must either: | ||
| a) Release your complete source code under AGPLv3, or | ||
| b) Obtain a commercial license. | ||
| This program is free software: you can redistribute it and/or modify | ||
| it under the terms of the GNU Affero General Public License as published | ||
| by the Free Software Foundation, either version 3 of the License, or | ||
| (at your option) any later version. | ||
| This program is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| GNU Affero General Public License for more details. | ||
| You should have received a copy of the GNU Affero General Public License | ||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
| AGPLv3 for personal use is free. Commercial licenses available. |
+7
-4
| { | ||
| "name": "@wipcomputer/universal-installer", | ||
| "version": "2.1.5", | ||
| "version": "2.2.0", | ||
| "type": "module", | ||
| "description": "The Universal Interface specification for agent-native software. Teaches your AI how to build repos with every interface: CLI, Module, MCP Server, OpenClaw Plugin, Skill, Claude Code Hook.", | ||
| "description": "Reference installer for agent-native software. Detects which of the eight Universal Interfaces a repo exposes (CLI, Module, MCP Server local stdio, Remote MCP, OpenClaw Plugin, Skill, Claude Code Hook, Claude Code Plugin) and installs them all. Primary install command is `ldm install`; `wip-install` is the standalone fallback.", | ||
| "main": "detect.mjs", | ||
@@ -25,4 +25,6 @@ "bin": { | ||
| "claude-code", | ||
| "claude-code-plugin", | ||
| "openclaw", | ||
| "mcp", | ||
| "remote-mcp", | ||
| "cli" | ||
@@ -34,5 +36,6 @@ ], | ||
| "type": "git", | ||
| "url": "git+https://github.com/wipcomputer/wip-universal-installer.git" | ||
| "url": "git+https://github.com/wipcomputer/wip-ai-devops-toolbox.git", | ||
| "directory": "tools/wip-universal-installer" | ||
| }, | ||
| "homepage": "https://github.com/wipcomputer/wip-universal-installer" | ||
| "homepage": "https://github.com/wipcomputer/wip-ai-devops-toolbox/tree/main/tools/wip-universal-installer" | ||
| } |
+27
-3
| ###### WIP Computer | ||
| [](https://www.npmjs.com/package/@wipcomputer/universal-installer) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/install.js) [](https://clawhub.ai/parkertoddbrooks/wip-universal-installer) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/SKILL.md) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/SPEC.md) | ||
| > **MOVED.** The Universal Installer has been moved into LDM OS. The canonical home is now: | ||
| > **https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/README.md** | ||
| > | ||
| > This copy is kept for back-compat against existing links and will be removed in a future cut. Update bookmarks and badge URLs. | ||
| [](https://www.npmjs.com/package/@wipcomputer/universal-installer) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/install.js) [](https://clawhub.ai/parkertoddbrooks/wip-universal-installer) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/SKILL.md) [](https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/SPEC.md) | ||
| # Universal Installer | ||
@@ -9,2 +14,16 @@ | ||
| ## The Badges | ||
| The chiclets at the top of this README tell you what interfaces this repo ships. Every repo that follows the Universal Interface Spec declares its interfaces the same way. | ||
| | Badge | What it means | | ||
| |-------|--------------| | ||
| | **npm** | Published to npm. Installable via `npm install`. Versioned, dependency-managed, standard distribution. | | ||
| | **CLI / TUI** | Ships a command-line interface. Humans run it in a terminal. Agents call it from shell. The most portable interface there is. | | ||
| | **OpenClaw Skill** | Registered as a skill on [ClawHub](https://clawhub.ai). OpenClaw agents can discover and use it natively through the gateway. | | ||
| | **Claude Code Skill** | Has a `SKILL.md` that teaches Claude Code (and any agent that reads markdown) when to use this tool, what it does, and how to call it. The agent reads the file and learns the capability. | | ||
| | **Universal Interface Spec** | Follows the [SPEC.md](SPEC.md) convention. The repo's architecture is documented, the interfaces are declared, and any agent or human can understand the full surface area by reading one file. | | ||
| When you see these badges on a WIP repo, you know exactly how to consume it. Human or agent, CLI or plugin, local or remote. That's the point. | ||
| ## The Problem | ||
@@ -61,4 +80,9 @@ | ||
| MIT | ||
| ``` | ||
| CLI, module, skills MIT (use anywhere, no restrictions) | ||
| Hosted or cloud service use AGPL (network service distribution) | ||
| ``` | ||
| Built by Parker Todd Brooks, with Claude Code and Lēsa (OpenClaw). | ||
| AGPL for personal use is free. | ||
| Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6). |
+45
-22
| ###### WIP Computer | ||
| > **MOVED.** Canonical reference is now: | ||
| > **https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/** | ||
| > | ||
| > This copy is kept for back-compat. Update bookmarks. | ||
| # Universal Installer ... Reference | ||
@@ -25,18 +31,20 @@ | ||
| Agents don't all speak the same language. Some run shell commands. Some import modules. Some talk MCP. Some read markdown instructions. | ||
| Agents don't all speak the same language. Some run shell commands. Some import modules. Some talk MCP over stdio. Some talk MCP over HTTPS. Some read markdown instructions. | ||
| So every tool should expose multiple interfaces into the same core logic: | ||
| So every tool should expose multiple interfaces into the same core logic. The canonical order: | ||
| | Interface | What | Who uses it | | ||
| |-----------|------|-------------| | ||
| | **CLI** | Shell command | Humans, any agent with bash | | ||
| | **Module** | ES import | Other tools, scripts | | ||
| | **MCP Server** | JSON-RPC over stdio | Claude Code, Cursor, any MCP client | | ||
| | **OpenClaw Plugin** | Lifecycle hooks + tools | OpenClaw agents | | ||
| | **Skill** | Markdown instructions (SKILL.md) | Any agent that reads files | | ||
| | **Claude Code Hook** | PreToolUse/Stop events | Claude Code | | ||
| | # | Interface | What | Who uses it | | ||
| |---|-----------|------|-------------| | ||
| | 1 | **CLI** | Shell command | Humans, any agent with bash | | ||
| | 2 | **Module** | ES import | Other tools, scripts | | ||
| | 3 | **MCP Server (local stdio)** | JSON-RPC over stdio | Claude Code, Cursor, OpenClaw | | ||
| | 4 | **Remote MCP** | JSON-RPC over HTTPS (SSE / streamable HTTP) | Claude Desktop, web, mobile | | ||
| | 5 | **OpenClaw Plugin** | Lifecycle hooks + tools | OpenClaw agents | | ||
| | 6 | **Skill** | Markdown instructions (`SKILL.md`; Codex reads `AGENTS.md` with same role) | Any agent that reads files | | ||
| | 7 | **Claude Code Hook** | PreToolUse/Stop events | Claude Code | | ||
| | 8 | **Claude Code Plugin** | Distributable package (skills, agents, hooks, MCP, LSP) | Claude Code marketplace | | ||
| Not every tool needs all six. Build what makes sense. But the more interfaces you expose, the more agents can use your tool. | ||
| Not every tool needs all eight. Build what makes sense. Local + Remote MCP sit next to each other because they are sibling transports. | ||
| See [SPEC.md](SPEC.md) for the full specification. | ||
| Canonical spec: [wip-ldm-os/docs/universal-installer/SPEC.md](https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/SPEC.md). The local `SPEC.md` in this folder is a back-compat stub. | ||
@@ -59,20 +67,33 @@ ## How to Build It | ||
| ## The Reference Installer | ||
| ## The Installer | ||
| `wip-install` scans any repo, detects which interfaces exist, and installs them all. One command. | ||
| `ldm install` is the primary installer (part of LDM OS). `wip-install` is the standalone fallback. Both scan a repo or slug, detect which interfaces exist, and install them all. | ||
| ```bash | ||
| # From GitHub | ||
| wip-install wipcomputer/wip-grok | ||
| # Primary (ldm install) | ||
| ldm install /path/to/repo # local | ||
| ldm install wipcomputer/wip-grok # from GitHub | ||
| ldm install <slug> # from catalog (stable, default) | ||
| ldm install --alpha <slug> # alpha (validation track) | ||
| ldm install --beta <slug> # beta (validation track) | ||
| ldm install <slug> --dry-run # detect only | ||
| # From a local path | ||
| # Fallback (wip-install standalone) | ||
| wip-install /path/to/repo | ||
| wip-install --dry-run /path/to/repo | ||
| wip-install --json /path/to/repo | ||
| ``` | ||
| # Detect only (no install) | ||
| wip-install --dry-run wipcomputer/wip-x | ||
| Tracks select the npm dist-tag (or git ref). The same install spec URL covers all three; the AI follows the spec, the user picks the track via flag. | ||
| # Machine-readable output | ||
| wip-install --json /path/to/repo | ||
| ### Install Spec URL | ||
| When telling a user how to install a product, point them at the install spec URL: | ||
| ``` | ||
| https://wip.computer/install/<slug>.txt | ||
| ``` | ||
| Agent-readable. Track-neutral. The user pastes one prompt into any AI; the AI reads the spec, explains the product, runs `ldm install --dry-run <slug>`, and only installs after explicit consent. Distinct from `agent.txt`, which is the site-level agent entrypoint. | ||
| ### Example Output | ||
@@ -99,6 +120,8 @@ | ||
| | `main` or `exports` in `package.json` | Module | Reports import path | | ||
| | `mcp-server.mjs` | MCP | Prints `.mcp.json` config | | ||
| | `mcp-server.mjs` | MCP (local stdio) | Adds `command` + `args` entry to `.mcp.json` | | ||
| | `mcp.remote.url` in `package.json` | Remote MCP | Adds `url` + `transport` entry to `.mcp.json`; prints Claude Desktop hint. *Implementation in flight ([master plan](https://github.com/wipcomputer/wip-ldm-os-private/blob/main/ai/product/bugs/installer/2026-04-28--cc-mini--installer-eight-interfaces-master-plan.md)).* | | ||
| | `openclaw.plugin.json` | OpenClaw | Copies to `~/.openclaw/extensions/` | | ||
| | `SKILL.md` | Skill | Reports path | | ||
| | `guard.mjs` or `claudeCode.hook` | CC Hook | Adds to `~/.claude/settings.json` | | ||
| | `.claude-plugin/plugin.json` | CC Plugin | Registers with Claude Code marketplace | | ||
@@ -105,0 +128,0 @@ ## Real Examples |
+100
-35
| --- | ||
| name: wip-universal-installer | ||
| version: 2.1.5 | ||
| description: The Universal Interface specification for agent-native software. Teaches your AI how to build repos with every interface. | ||
| homepage: https://github.com/wipcomputer/wip-universal-installer | ||
| description: Reference installer for agent-native software. Scans a repo, detects which of the eight Universal Interfaces it exposes, and installs them all. Primary install command is `ldm install`; `wip-install` is the standalone fallback. | ||
| license: MIT | ||
| interface: [cli, module, skill] | ||
| metadata: | ||
| display-name: "Universal Installer" | ||
| version: "2.1.5" | ||
| homepage: "https://github.com/wipcomputer/wip-universal-installer" | ||
| author: "Parker Todd Brooks" | ||
| category: dev-tools | ||
@@ -11,13 +15,20 @@ capabilities: | ||
| - install-cli | ||
| - install-mcp | ||
| - install-mcp-local | ||
| - install-mcp-remote | ||
| - install-openclaw-plugin | ||
| - install-claude-code-hook | ||
| dependencies: [] | ||
| interface: CLI | ||
| openclaw: | ||
| emoji: "🔌" | ||
| install: | ||
| env: [] | ||
| author: | ||
| name: Parker Todd Brooks | ||
| - install-claude-code-plugin | ||
| requires: | ||
| bins: [node, npm, git] | ||
| openclaw: | ||
| requires: | ||
| bins: [node, npm, git] | ||
| install: | ||
| - id: node | ||
| kind: node | ||
| package: "@wipcomputer/universal-installer" | ||
| bins: [wip-install] | ||
| label: "Install via npm" | ||
| emoji: "🔌" | ||
| compatibility: Requires git, npm, node. Node.js 18+. | ||
| --- | ||
@@ -27,33 +38,67 @@ | ||
| Reference installer for agent-native software. Scans a repo, detects which interfaces it exposes, and installs them all. | ||
| > **MOVED.** Canonical home is now: | ||
| > **https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/** | ||
| > | ||
| > This copy is kept for back-compat. Update references. | ||
| Reference installer for agent-native software. Scans a repo, detects which of the eight Universal Interfaces it exposes, and installs them all. | ||
| ## When to Use This Skill | ||
| **Use wip-install for:** | ||
| - Installing any repo that follows the Universal Interface pattern | ||
| - Detecting what interfaces a repo provides | ||
| - Setting up CLI tools, MCP servers, OpenClaw plugins, and Claude Code hooks in one command | ||
| **Use `ldm install` (primary) for:** | ||
| - Installing any product that follows the Universal Interface pattern | ||
| - Installing from a slug (catalog), a local path, or a GitHub `org/repo` | ||
| - Selecting a release track: stable (default), `--alpha`, `--beta` | ||
| - Doing a `--dry-run` first to show the user exactly what will change | ||
| **Use detect.mjs for:** | ||
| **Use `wip-install` (standalone fallback) for:** | ||
| - Bootstrapping into a system that does not have LDM OS yet | ||
| - One-shot installs in CI or a temporary shell | ||
| - JSON output for tooling: `wip-install --json /path/to/repo` | ||
| **Use `detect.mjs` (module) for:** | ||
| - Programmatically detecting interfaces in a repo | ||
| - Building custom installers or CI pipelines | ||
| - Validating that a repo follows the spec | ||
| - Validating that a repo follows the Universal Interface spec | ||
| ### Do NOT Use For | ||
| - Installing standard npm packages (use npm directly) | ||
| - Repos that don't follow the Universal Interface conventions | ||
| - Building or compiling code (this only installs) | ||
| - Installing arbitrary npm packages (use `npm` directly). | ||
| - Repos that do not follow the Universal Interface conventions. | ||
| - Building or compiling code (this only installs). | ||
| ## Install Spec URL pattern | ||
| When a user asks "how do I install X?", point them at the install spec URL: | ||
| ``` | ||
| https://wip.computer/install/<slug>.txt | ||
| ``` | ||
| The spec is agent-readable. The user pastes one prompt into any AI; the AI reads the spec, explains the product, runs `ldm install --dry-run <slug>`, and only installs after explicit consent. Tracks (`--alpha`, `--beta`, stable) are selected by flag against the same URL. | ||
| The install spec URL is **per-product**. The site-level agent entrypoint is `agent.txt` (different scope; can point at install specs). | ||
| ## API Reference | ||
| ### CLI | ||
| ### CLI ... primary | ||
| ```bash | ||
| wip-install /path/to/repo # install all interfaces | ||
| wip-install org/repo # clone from GitHub + install | ||
| wip-install --dry-run /path/to/repo # detect only, no changes | ||
| wip-install --json /path/to/repo # JSON output | ||
| ldm install /path/to/repo # local | ||
| ldm install org/repo # from GitHub | ||
| ldm install <slug> # from catalog (stable, default) | ||
| ldm install --alpha <slug> # alpha (validation track) | ||
| ldm install --beta <slug> # beta (validation track) | ||
| ldm install <slug> --dry-run # detect only, no changes | ||
| ``` | ||
| ### CLI ... fallback | ||
| ```bash | ||
| wip-install /path/to/repo # standalone | ||
| wip-install --dry-run /path/to/repo # detect only | ||
| wip-install --json /path/to/repo # machine-readable | ||
| wip-install org/repo # clone from GitHub + install | ||
| ``` | ||
| ### Module (detect.mjs) | ||
@@ -71,11 +116,31 @@ | ||
| ## Universal Interface | ||
| ## The Eight Universal Interfaces | ||
| See [SPEC.md](https://github.com/wipcomputer/wip-universal-installer/blob/main/SPEC.md) for the full specification. | ||
| Canonical order. Local + Remote MCP sit next to each other because they are sibling transports. CC Plugin sits last because it bundles the others. | ||
| 1. **CLI** ... `package.json` bin field | ||
| 2. **Module** ... `package.json` main/exports | ||
| 3. **MCP Server** ... `mcp-server.mjs` | ||
| 4. **OpenClaw Plugin** ... `openclaw.plugin.json` | ||
| 5. **Skill** ... `SKILL.md` | ||
| 6. **Claude Code Hook** ... `guard.mjs` or `claudeCode.hook` | ||
| 1. **CLI** ... `package.json` `bin` field | ||
| 2. **Module** ... `package.json` `main` / `exports` | ||
| 3. **MCP Server (local stdio)** ... `mcp-server.mjs` | ||
| 4. **Remote MCP** ... `mcp.remote = { url, transport, auth }` in `package.json`. *Detection + install action in flight; see master plan.* | ||
| 5. **OpenClaw Plugin** ... `openclaw.plugin.json` | ||
| 6. **Skill** ... `SKILL.md` (Codex CLI reads `AGENTS.md` with the same role and content shape; treat as a platform variant of this same interface, not a separate one) | ||
| 7. **Claude Code Hook** ... `guard.mjs` or `claudeCode.hook` in `package.json` | ||
| 8. **Claude Code Plugin** ... `.claude-plugin/plugin.json` | ||
| Full spec: [wip-ldm-os/docs/universal-installer/SPEC.md](https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/SPEC.md). | ||
| > *Disposable, agent-generated artifacts such as custom dashboards or one-off scripts are outputs of the agent composing Universal Interface products, not additional Universal Interface types; see the [canonical LDM OS Universal Installer spec](https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/SPEC.md) for the full architecture layers and primary flow.* | ||
| ## Remote MCP contract (pinned) | ||
| > Remote MCP endpoint is **declared by package/catalog metadata** and **registered by `ldm install`**. | ||
| No filesystem-sniffing fallback. The repo opts in by writing the field. The catalog can override `url` if the package ships a placeholder. | ||
| ## Future considerations | ||
| *LSP as a standalone interface (#9).* LSP servers are currently surfaced via Claude Code Plugin bundles (#8) ... `.lsp.json` is part of the plugin shape. If a product ships a standalone LSP server outside a CC Plugin, we will add it as #9. Not added today because no WIP product ships one yet. | ||
| ## Reference | ||
| See [REFERENCE.md](REFERENCE.md) for sensors/actuators, full interface table with detection patterns, build guidance, and real examples. |
+27
-145
| # The Universal Interface Specification | ||
| Every tool is a sensor, an actuator, or both. Every tool should be accessible through multiple interfaces. We call this the Universal Interface. | ||
| > **MOVED.** The canonical home of this spec is now: | ||
| > **https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/SPEC.md** | ||
| > | ||
| > This copy is kept for back-compat against existing links. The full spec, including the eight interfaces, the install-spec URL convention, and the Remote MCP contract, lives there. | ||
| This is the spec. | ||
| ## Summary (canonical text lives upstream) | ||
| ## The Six Interfaces | ||
| The Universal Interface defines **eight interfaces** in canonical order: | ||
| ### 1. CLI | ||
| 1. CLI | ||
| 2. Module | ||
| 3. MCP Server (local stdio) | ||
| 4. **Remote MCP** (HTTP/SSE or streamable HTTP) | ||
| 5. OpenClaw Plugin | ||
| 6. Skill (SKILL.md) | ||
| 7. Claude Code Hook | ||
| 8. Claude Code Plugin | ||
| A shell command. The most universal interface. If it has a terminal, it works. | ||
| Local + Remote MCP sit next to each other because they are sibling transports. CC Plugin sits last because it bundles the others. | ||
| **Convention:** `package.json` with a `bin` field. | ||
| ### Remote MCP contract (pinned) | ||
| **Detection:** `pkg.bin` exists. | ||
| > Remote MCP endpoint is **declared by package/catalog metadata** and **registered by `ldm install`**. | ||
| **Install:** `npm install -g .` or `npm link`. | ||
| Convention: `mcp.remote = { url, transport, auth }` in `package.json`. No filesystem-sniffing fallback. | ||
| ```json | ||
| { | ||
| "bin": { | ||
| "wip-grok": "./cli.mjs" | ||
| } | ||
| } | ||
| ``` | ||
| ### Install Spec URL | ||
| ### 2. Module | ||
| Every product gets an agent-readable install runbook published at `https://wip.computer/install/<slug>.txt`. Track-neutral; tracks (`--alpha`, `--beta`, stable) are selected by flag against the same URL. | ||
| An importable ES module. The programmatic interface. Other tools compose with it. | ||
| ### Reference | ||
| **Convention:** `package.json` with `main` or `exports` field. File is `core.mjs` by convention. | ||
| For the full spec ... interfaces in detail, architecture layers, install spec URL behavior contract, agent.txt distinction, examples ... read the canonical doc: | ||
| **Detection:** `pkg.main` or `pkg.exports` exists. | ||
| - [`wip-ldm-os/docs/universal-installer/SPEC.md`](https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/SPEC.md) | ||
| - [`wip-ldm-os/docs/universal-installer/TECHNICAL.md`](https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/TECHNICAL.md) | ||
| - [`wip-ldm-os/docs/universal-installer/README.md`](https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/README.md) | ||
| **Install:** `npm install <package>` or import directly from path. | ||
| For the master plan and implementation tickets: | ||
| ```json | ||
| { | ||
| "type": "module", | ||
| "main": "core.mjs", | ||
| "exports": { | ||
| ".": "./core.mjs", | ||
| "./cli": "./cli.mjs" | ||
| } | ||
| } | ||
| ``` | ||
| ### 3. MCP Server | ||
| A JSON-RPC server implementing the Model Context Protocol. Any MCP-compatible agent can use it. | ||
| **Convention:** `mcp-server.mjs` (or `.js`, `.ts`) at the repo root. Uses `@modelcontextprotocol/sdk`. | ||
| **Detection:** One of `mcp-server.mjs`, `mcp-server.js`, `mcp-server.ts`, `dist/mcp-server.js` exists. | ||
| **Install:** Add to `.mcp.json`: | ||
| ```json | ||
| { | ||
| "tool-name": { | ||
| "command": "node", | ||
| "args": ["/path/to/mcp-server.mjs"] | ||
| } | ||
| } | ||
| ``` | ||
| ### 4. OpenClaw Plugin | ||
| A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings. | ||
| **Convention:** `openclaw.plugin.json` at the repo root. | ||
| **Detection:** `openclaw.plugin.json` exists. | ||
| **Install:** Copy to `~/.openclaw/extensions/<name>/`, run `npm install --omit=dev`. | ||
| ### 5. Skill (SKILL.md) | ||
| A markdown file that teaches agents when and how to use the tool. The instruction interface. | ||
| **Convention:** `SKILL.md` at the repo root. YAML frontmatter with name, version, description, metadata. | ||
| **Detection:** `SKILL.md` exists. | ||
| **Install:** Referenced by path. Agents read it when they need the tool. | ||
| ```yaml | ||
| --- | ||
| name: wip-grok | ||
| version: 1.0.0 | ||
| description: xAI Grok API. Search the web, search X, generate images. | ||
| metadata: | ||
| category: search,media | ||
| capabilities: | ||
| - web-search | ||
| - image-generation | ||
| --- | ||
| ``` | ||
| ### 6. Claude Code Hook | ||
| A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.). | ||
| **Convention:** `guard.mjs` at repo root, or `claudeCode.hook` in `package.json`. | ||
| **Detection:** `guard.mjs` exists, or `pkg.claudeCode.hook` is defined. | ||
| **Install:** Added to `~/.claude/settings.json` under `hooks`. | ||
| ```json | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [{ | ||
| "matcher": "Edit|Write", | ||
| "hooks": [{ | ||
| "type": "command", | ||
| "command": "node /path/to/guard.mjs", | ||
| "timeout": 5 | ||
| }] | ||
| }] | ||
| } | ||
| } | ||
| ``` | ||
| ## Architecture | ||
| Every repo that follows this spec has the same basic structure: | ||
| ``` | ||
| your-tool/ | ||
| core.mjs pure logic, zero or minimal deps | ||
| cli.mjs thin CLI wrapper around core | ||
| mcp-server.mjs MCP server wrapping core functions as tools | ||
| SKILL.md agent instructions with YAML frontmatter | ||
| package.json name, bin, main, exports, type: module | ||
| README.md human documentation | ||
| ``` | ||
| Not every tool needs all six interfaces. Build the ones that make sense. | ||
| The minimum viable agent-native tool has two interfaces: **Module** (importable) and **Skill** (agent instructions). Add CLI for humans. Add MCP for agents that speak MCP. Add OpenClaw/CC Hook for specific platforms. | ||
| ## The Reference Installer | ||
| `wip-install` is the reference implementation. It scans a repo, detects which interfaces exist, and installs them all. One command. | ||
| ```bash | ||
| wip-install /path/to/repo # local | ||
| wip-install org/repo # from GitHub | ||
| wip-install --dry-run /path/to/repo # detect only | ||
| wip-install --json /path/to/repo # JSON output | ||
| ``` | ||
| ## Examples | ||
| | Repo | Interfaces | Type | | ||
| |------|------------|------| | ||
| | [wip-grok](https://github.com/wipcomputer/wip-grok) | CLI + Module + MCP + Skill | Sensor + Actuator | | ||
| | [wip-x](https://github.com/wipcomputer/wip-x) | CLI + Module + MCP + Skill | Sensor + Actuator | | ||
| | [wip-file-guard](https://github.com/wipcomputer/wip-file-guard) | CLI + OpenClaw + CC Hook | Actuator | | ||
| | [wip-markdown-viewer](https://github.com/wipcomputer/wip-markdown-viewer) | CLI + Module | Actuator | | ||
| - [`wip-ldm-os-private/ai/product/bugs/installer/`](https://github.com/wipcomputer/wip-ldm-os-private/tree/main/ai/product/bugs/installer) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
64834
102.82%16
6.67%968
159.52%87
38.1%2
-33.33%1
Infinity%80
-20%1
Infinity%