@morphllm/morph-setup
Advanced tools
+215
-11
@@ -7,3 +7,3 @@ #!/usr/bin/env node | ||
| import * as p3 from "@clack/prompts"; | ||
| import path7 from "path"; | ||
| import path8 from "path"; | ||
| import { fileURLToPath } from "url"; | ||
@@ -518,2 +518,52 @@ import { createRequire as createRequire2 } from "module"; | ||
| } | ||
| async function removeSkillForAgent(skillName, agentType, options = {}) { | ||
| let target; | ||
| try { | ||
| target = getInstallPath(skillName, agentType, options); | ||
| } catch { | ||
| return false; | ||
| } | ||
| try { | ||
| await lstat(target); | ||
| } catch { | ||
| return false; | ||
| } | ||
| await rm2(target, { recursive: true, force: true }); | ||
| return true; | ||
| } | ||
| async function removeCanonicalSkill(skillName, options = {}) { | ||
| let target; | ||
| try { | ||
| target = getCanonicalPath(skillName, options); | ||
| } catch { | ||
| return false; | ||
| } | ||
| try { | ||
| await lstat(target); | ||
| } catch { | ||
| return false; | ||
| } | ||
| await rm2(target, { recursive: true, force: true }); | ||
| return true; | ||
| } | ||
| function getInstallPath(skillName, agentType, options = {}) { | ||
| const agent = agents[agentType]; | ||
| const cwd = options.cwd || process.cwd(); | ||
| const sanitized = sanitizeName(skillName); | ||
| const targetBase = options.global ? agent.globalSkillsDir : join4(cwd, agent.skillsDir); | ||
| const installPath = join4(targetBase, sanitized); | ||
| if (!isPathSafe(targetBase, installPath)) { | ||
| throw new Error("Invalid skill name: potential path traversal detected"); | ||
| } | ||
| return installPath; | ||
| } | ||
| function getCanonicalPath(skillName, options = {}) { | ||
| const sanitized = sanitizeName(skillName); | ||
| const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd); | ||
| const canonicalPath = join4(canonicalBase, sanitized); | ||
| if (!isPathSafe(canonicalBase, canonicalPath)) { | ||
| throw new Error("Invalid skill name: potential path traversal detected"); | ||
| } | ||
| return canonicalPath; | ||
| } | ||
@@ -538,7 +588,7 @@ // src/mcp-install.ts | ||
| function getWindsurfConfigPath() { | ||
| const os2 = platform2(); | ||
| if (os2 === "darwin" || os2 === "linux") { | ||
| const os3 = platform2(); | ||
| if (os3 === "darwin" || os3 === "linux") { | ||
| return expandHome("~/.codeium/windsurf/mcp_config.json"); | ||
| } | ||
| if (os2 === "win32") { | ||
| if (os3 === "win32") { | ||
| const appData = process.env.APPDATA; | ||
@@ -554,7 +604,7 @@ if (!appData) return null; | ||
| function getAmpSettingsPath() { | ||
| const os2 = platform2(); | ||
| if (os2 === "darwin" || os2 === "linux") { | ||
| const os3 = platform2(); | ||
| if (os3 === "darwin" || os3 === "linux") { | ||
| return path.join(homedir3(), ".config", "amp", "settings.json"); | ||
| } | ||
| if (os2 === "win32") { | ||
| if (os3 === "win32") { | ||
| return path.join(homedir3(), ".config", "amp", "settings.json"); | ||
@@ -1077,2 +1127,11 @@ } | ||
| } | ||
| function computeStrippedClaudeMd(existing) { | ||
| let stripped = existing; | ||
| for (const pattern of LEGACY_BLOCK_PATTERNS) { | ||
| stripped = stripped.replace(pattern, ""); | ||
| } | ||
| stripped = stripped.replace(/\n{3,}/g, "\n\n").replace(/\s+$/, ""); | ||
| const next = stripped.length === 0 ? "" : stripped + "\n"; | ||
| return next === existing ? null : next; | ||
| } | ||
| async function injectCompactInstructions() { | ||
@@ -1121,2 +1180,25 @@ let existing = ""; | ||
| } | ||
| async function uninstallClaudeCodePlugin() { | ||
| const pluginRemoved = PLUGIN_PATHS_TO_CLEAN.some((dir) => existsSync4(dir)); | ||
| await cleanupExistingPlugin(); | ||
| let claudeMdCleaned = false; | ||
| try { | ||
| const existing = await fs3.readFile(CLAUDE_MD_PATH, "utf-8"); | ||
| const next = computeStrippedClaudeMd(existing); | ||
| if (next !== null) { | ||
| await fs3.writeFile(CLAUDE_MD_PATH, next, "utf-8"); | ||
| claudeMdCleaned = true; | ||
| } | ||
| } catch { | ||
| } | ||
| let stateRemoved = false; | ||
| if (existsSync4(MORPH_STATE_DIR)) { | ||
| try { | ||
| await fs3.rm(MORPH_STATE_DIR, { recursive: true, force: true }); | ||
| stateRemoved = true; | ||
| } catch { | ||
| } | ||
| } | ||
| return { pluginRemoved, claudeMdCleaned, stateRemoved }; | ||
| } | ||
@@ -1195,2 +1277,32 @@ // src/opencode-plugin.ts | ||
| } | ||
| async function uninstallOpenCodePlugin() { | ||
| if (!existsSync5(OPENCODE_CONFIG_PATH)) return false; | ||
| let data; | ||
| try { | ||
| data = JSON52.parse(await fs4.readFile(OPENCODE_CONFIG_PATH, "utf-8")); | ||
| } catch { | ||
| return false; | ||
| } | ||
| if (!isObject2(data)) return false; | ||
| const obj = data; | ||
| let changed = false; | ||
| if (Array.isArray(obj.plugin)) { | ||
| const next = obj.plugin.filter((x) => x !== PLUGIN_PACKAGE); | ||
| if (next.length !== obj.plugin.length) { | ||
| obj.plugin = next; | ||
| changed = true; | ||
| } | ||
| } | ||
| if (Array.isArray(obj.instructions)) { | ||
| const next = obj.instructions.filter((x) => x !== PLUGIN_INSTRUCTIONS_PATH); | ||
| if (next.length !== obj.instructions.length) { | ||
| obj.instructions = next; | ||
| changed = true; | ||
| } | ||
| } | ||
| if (changed) { | ||
| await fs4.writeFile(OPENCODE_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", "utf-8"); | ||
| } | ||
| return changed; | ||
| } | ||
@@ -1278,5 +1390,45 @@ // src/mcp-reset.ts | ||
| // src/uninstall.ts | ||
| import { existsSync as existsSync6 } from "fs"; | ||
| import os2 from "os"; | ||
| import path7 from "path"; | ||
| function managedMcpRoot() { | ||
| return path7.join(os2.homedir(), ".morph", "mcp"); | ||
| } | ||
| async function uninstallMorph(opts) { | ||
| const global = opts.global ?? true; | ||
| const mcpRemoved = []; | ||
| for (const client of opts.clients) { | ||
| if (await resetMorphMcpFromClient(client)) mcpRemoved.push(client); | ||
| } | ||
| const skillNames = (await discoverSkills(opts.bundledSkillsDir)).map(getSkillDisplayName); | ||
| const skillsRemoved = []; | ||
| for (const agent of opts.agents) { | ||
| for (const skill of skillNames) { | ||
| if (await removeSkillForAgent(skill, agent, { global })) { | ||
| skillsRemoved.push({ agent, skill }); | ||
| } | ||
| } | ||
| } | ||
| const canonicalSkillsRemoved = []; | ||
| for (const skill of skillNames) { | ||
| if (await removeCanonicalSkill(skill, { global })) canonicalSkillsRemoved.push(skill); | ||
| } | ||
| const claudePlugin = opts.clients.includes("claude-code") ? await uninstallClaudeCodePlugin() : void 0; | ||
| const openCodePluginRemoved = opts.clients.includes("opencode") ? await uninstallOpenCodePlugin() : false; | ||
| const managedInstallRemoved = existsSync6(managedMcpRoot()); | ||
| await clearManagedMcpInstalls(); | ||
| return { | ||
| mcpRemoved, | ||
| skillsRemoved, | ||
| canonicalSkillsRemoved, | ||
| claudePlugin, | ||
| openCodePluginRemoved, | ||
| managedInstallRemoved | ||
| }; | ||
| } | ||
| // src/index.ts | ||
| var __filename = fileURLToPath(import.meta.url); | ||
| var __dirname = path7.dirname(__filename); | ||
| var __dirname = path8.dirname(__filename); | ||
| var require2 = createRequire2(import.meta.url); | ||
@@ -1313,3 +1465,3 @@ var MORPH_ASCII_LINES = [ | ||
| function getBundledSkillsDir() { | ||
| return path7.resolve(__dirname, "..", ".agents", "skills"); | ||
| return path8.resolve(__dirname, "..", ".agents", "skills"); | ||
| } | ||
@@ -1319,5 +1471,9 @@ function getAllAgentTypes() { | ||
| } | ||
| function getAllPlatforms() { | ||
| const supportedMcpIds = new Set(listSupportedMcpClients().map((c) => c.id)); | ||
| return getAllAgentTypes().filter((id) => supportedMcpIds.has(id)); | ||
| } | ||
| async function selectPlatforms(opts) { | ||
| const supportedMcpIds = new Set(listSupportedMcpClients().map((c) => c.id)); | ||
| const allPlatforms = getAllAgentTypes().filter((id) => supportedMcpIds.has(id)); | ||
| const allPlatforms = getAllPlatforms(); | ||
| if (opts.cliAgents.length > 0) return opts.cliAgents; | ||
@@ -1452,2 +1608,43 @@ if (opts.yes) { | ||
| } | ||
| async function runUninstall(opts) { | ||
| p3.intro(chalk3.greenBright(" Morph Uninstall ")); | ||
| const platforms = opts.cliAgents.length > 0 ? opts.cliAgents : getAllPlatforms(); | ||
| if (!opts.yes) { | ||
| const labels = platforms.map((id) => agents[id]?.displayName ?? id).join(", "); | ||
| const ok = await p3.confirm({ | ||
| message: `Remove Morph (MCP servers, skills, and plugins) from: ${labels}?`, | ||
| initialValue: true | ||
| }); | ||
| if (p3.isCancel(ok) || !ok) { | ||
| p3.outro("Nothing removed."); | ||
| return; | ||
| } | ||
| } | ||
| const spin = p3.spinner(); | ||
| spin.start("Removing Morph from your agents"); | ||
| const report = await uninstallMorph({ | ||
| clients: platforms, | ||
| agents: platforms, | ||
| bundledSkillsDir: getBundledSkillsDir(), | ||
| global: true | ||
| }); | ||
| spin.stop("Removed Morph"); | ||
| if (report.mcpRemoved.length > 0) { | ||
| p3.log.success(`MCP config removed from: ${chalk3.dim(report.mcpRemoved.join(", "))}`); | ||
| } | ||
| if (report.skillsRemoved.length > 0) { | ||
| const perAgent = /* @__PURE__ */ new Map(); | ||
| for (const { agent } of report.skillsRemoved) perAgent.set(agent, (perAgent.get(agent) ?? 0) + 1); | ||
| const summary = [...perAgent].map(([a, n]) => `${a} (${n})`).join(", "); | ||
| p3.log.success(`Skills removed from: ${chalk3.dim(summary)}`); | ||
| } | ||
| if (report.claudePlugin?.pluginRemoved) p3.log.success("Claude Code plugin (morph-compact) removed"); | ||
| if (report.claudePlugin?.claudeMdCleaned) p3.log.success("Compact instructions stripped from ~/.claude/CLAUDE.md"); | ||
| if (report.openCodePluginRemoved) p3.log.success("OpenCode plugin deregistered"); | ||
| if (report.managedInstallRemoved) p3.log.success("Managed Morph MCP server install removed (~/.morph/mcp)"); | ||
| const nothing = report.mcpRemoved.length === 0 && report.skillsRemoved.length === 0 && !report.claudePlugin?.pluginRemoved && !report.claudePlugin?.claudeMdCleaned && !report.openCodePluginRemoved && !report.managedInstallRemoved; | ||
| if (nothing) p3.log.info("No Morph artifacts were found to remove."); | ||
| track({ event: "uninstall", agents: platforms.join(",") }); | ||
| p3.outro(chalk3.greenBright("Morph removed.")); | ||
| } | ||
| async function main() { | ||
@@ -1457,3 +1654,3 @@ const pkg = require2("../package.json"); | ||
| await showMorphSplash(); | ||
| const program = new Command().name("morph-setup").description("Install Morph MCP and bundled skills onto coding agents").argument("[source]", "Optional skills source (repo/path). Bundled skills are always installed.").option("--morph-api-key <key>", "MORPH_API_KEY to write into MCP config (overrides env)").option("-g, --global", "Install skills globally (default)").option("-a, --agent <agents...>", "Target specific agents (repeatable)").option("-s, --skill <skills...>", "Install specific skills by name (only affects <source> skills)").option("-l, --list", "List available skills without installing").option("-y, --yes", "Skip all confirmation prompts").option("--exclude-tools <tools...>", "Disable specific MCP tools (e.g. --exclude-tools edit_file)").option("--fresh", "Remove existing Morph configs and npx cache before installing (clean slate)").option("--a2a-review", "Set up your AI code review twin").option("--github-username <username>", "GitHub username (for --a2a-review, skips auto-detection)").option("--claude-md <paths...>", "Paths to CLAUDE.md files (for --a2a-review, skips auto-scan)").version(pkg.version); | ||
| const program = new Command().name("morph-setup").description("Install Morph MCP and bundled skills onto coding agents").argument("[source]", "Optional skills source (repo/path). Bundled skills are always installed.").option("--morph-api-key <key>", "MORPH_API_KEY to write into MCP config (overrides env)").option("-g, --global", "Install skills globally (default)").option("-a, --agent <agents...>", "Target specific agents (repeatable)").option("-s, --skill <skills...>", "Install specific skills by name (only affects <source> skills)").option("-l, --list", "List available skills without installing").option("-y, --yes", "Skip all confirmation prompts").option("--exclude-tools <tools...>", "Disable specific MCP tools (e.g. --exclude-tools edit_file)").option("--fresh", "Remove existing Morph configs and npx cache before installing (clean slate)").option("--uninstall", "Remove Morph (MCP servers, skills, plugins) from all agents (or -a <agents>)").option("--a2a-review", "Set up your AI code review twin").option("--github-username <username>", "GitHub username (for --a2a-review, skips auto-detection)").option("--claude-md <paths...>", "Paths to CLAUDE.md files (for --a2a-review, skips auto-scan)").version(pkg.version); | ||
| const argv = process.argv.slice(); | ||
@@ -1463,2 +1660,9 @@ if (argv[2] === "--") argv.splice(2, 1); | ||
| const opts = program.opts(); | ||
| if (opts.uninstall) { | ||
| await runUninstall({ | ||
| yes: !!opts.yes, | ||
| cliAgents: (opts.agent ?? []).map((a) => a) | ||
| }); | ||
| return; | ||
| } | ||
| if (opts.a2aReview) { | ||
@@ -1465,0 +1669,0 @@ const { runA2aReviewSetup } = await import("./a2a-review-GIEEOXF6.js"); |
+1
-1
| { | ||
| "name": "@morphllm/morph-setup", | ||
| "version": "1.0.31", | ||
| "version": "1.0.32", | ||
| "description": "Install Morph MCP and bundled skills onto coding agents", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances 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
78568
10.25%1973
11.47%