refactor: consolidate agent/editor plugins under integrations/ (#5491)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
copyFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
} from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { homedir } from "os";
|
||||
|
||||
const PLUGIN_NAME = "@mem0/opencode-plugin";
|
||||
const MCP_CONFIG = {
|
||||
mem0: {
|
||||
type: "remote",
|
||||
url: "https://mcp.mem0.ai/mcp/",
|
||||
headers: {
|
||||
Authorization: "Token {env:MEM0_API_KEY}",
|
||||
},
|
||||
oauth: false,
|
||||
},
|
||||
};
|
||||
|
||||
const SKILLS_NAMESPACE = "mem0";
|
||||
|
||||
function getConfigDir(): string {
|
||||
const dir = join(homedir(), ".config", "opencode");
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function getConfigPath(): string {
|
||||
const configDir = getConfigDir();
|
||||
const jsonc = join(configDir, "opencode.jsonc");
|
||||
if (existsSync(jsonc)) return jsonc;
|
||||
return join(configDir, "opencode.json");
|
||||
}
|
||||
|
||||
function stripJsonComments(text: string): string {
|
||||
let result = "";
|
||||
let i = 0;
|
||||
let inString = false;
|
||||
let escape = false;
|
||||
while (i < text.length) {
|
||||
const ch = text[i];
|
||||
if (escape) {
|
||||
result += ch;
|
||||
escape = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (inString) {
|
||||
if (ch === "\\") escape = true;
|
||||
else if (ch === '"') inString = false;
|
||||
result += ch;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inString = true;
|
||||
result += ch;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && text[i + 1] === "/") {
|
||||
while (i < text.length && text[i] !== "\n") i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && text[i + 1] === "*") {
|
||||
i += 2;
|
||||
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
result += ch;
|
||||
i++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolvePluginDir(): string {
|
||||
try {
|
||||
return dirname(new URL(import.meta.url).pathname);
|
||||
} catch {}
|
||||
return __dirname ?? process.cwd();
|
||||
}
|
||||
|
||||
function findSkillsDir(): string {
|
||||
const base = resolvePluginDir();
|
||||
const candidates = [
|
||||
join(base, "opencode-skills"),
|
||||
join(dirname(base), "opencode-skills"),
|
||||
join(base, "..", "opencode-skills"),
|
||||
];
|
||||
for (const c of candidates) {
|
||||
try {
|
||||
if (existsSync(c) && statSync(c).isDirectory()) return c;
|
||||
} catch {}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function installSkills(): number {
|
||||
const skillsSource = findSkillsDir();
|
||||
if (!skillsSource) {
|
||||
console.log(" ! Skills directory not found — skipping slash command install");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const skillsTarget = join(getConfigDir(), "skills");
|
||||
if (!existsSync(skillsTarget)) mkdirSync(skillsTarget, { recursive: true });
|
||||
|
||||
let count = 0;
|
||||
const entries = readdirSync(skillsSource);
|
||||
|
||||
for (const name of entries) {
|
||||
const skillDir = join(skillsSource, name);
|
||||
try {
|
||||
if (!statSync(skillDir).isDirectory()) continue;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const skillFile = join(skillDir, "SKILL.md");
|
||||
if (!existsSync(skillFile)) continue;
|
||||
|
||||
const targetDir = join(skillsTarget, `${SKILLS_NAMESPACE}-${name}`);
|
||||
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
copyFileSync(skillFile, join(targetDir, "SKILL.md"));
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function uninstallSkills(): number {
|
||||
const skillsTarget = join(getConfigDir(), "skills");
|
||||
if (!existsSync(skillsTarget)) return 0;
|
||||
|
||||
let count = 0;
|
||||
const entries = readdirSync(skillsTarget);
|
||||
|
||||
for (const name of entries) {
|
||||
if (!name.startsWith(`${SKILLS_NAMESPACE}-`)) continue;
|
||||
const fullPath = join(skillsTarget, name);
|
||||
try {
|
||||
if (!statSync(fullPath).isDirectory()) continue;
|
||||
rmSync(fullPath, { recursive: true });
|
||||
count++;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function install() {
|
||||
console.log("Installing Mem0 plugin for OpenCode...\n");
|
||||
|
||||
const configPath = getConfigPath();
|
||||
let config: any = {};
|
||||
|
||||
if (existsSync(configPath)) {
|
||||
try {
|
||||
const raw = readFileSync(configPath, "utf-8");
|
||||
config = JSON.parse(stripJsonComments(raw));
|
||||
} catch {
|
||||
console.log(` ! Could not parse ${configPath}, creating fresh config`);
|
||||
config = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(config.plugin)) config.plugin = [];
|
||||
if (!config.plugin.includes(PLUGIN_NAME)) {
|
||||
config.plugin.push(PLUGIN_NAME);
|
||||
console.log(` + Added "${PLUGIN_NAME}" to plugin array`);
|
||||
} else {
|
||||
console.log(` ~ "${PLUGIN_NAME}" already in plugin array`);
|
||||
}
|
||||
|
||||
if (!config.mcp) config.mcp = {};
|
||||
if (!config.mcp.mem0) {
|
||||
config.mcp.mem0 = MCP_CONFIG.mem0;
|
||||
console.log(" + Added mem0 MCP server config");
|
||||
} else {
|
||||
console.log(" ~ mem0 MCP server already configured");
|
||||
}
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
||||
console.log(`\n Wrote ${configPath}`);
|
||||
|
||||
const skillCount = installSkills();
|
||||
if (skillCount > 0) {
|
||||
console.log(` + Installed ${skillCount} slash commands to ~/.config/opencode/skills/`);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
|
||||
if (!process.env.MEM0_API_KEY) {
|
||||
console.log(" ! MEM0_API_KEY is not set in your environment.");
|
||||
console.log(
|
||||
' Run: echo \'export MEM0_API_KEY="m0-your-key"\' >> ~/.zshrc && source ~/.zshrc',
|
||||
);
|
||||
console.log(
|
||||
" Get a free key at: https://app.mem0.ai/dashboard/api-keys\n",
|
||||
);
|
||||
} else {
|
||||
console.log(" MEM0_API_KEY detected\n");
|
||||
}
|
||||
|
||||
console.log("Done! Restart OpenCode to activate Mem0.");
|
||||
console.log(" Then run /mem0-onboard in the TUI to complete setup.\n");
|
||||
}
|
||||
|
||||
function uninstall() {
|
||||
const configPath = getConfigPath();
|
||||
if (!existsSync(configPath)) {
|
||||
console.log("No OpenCode config found. Nothing to remove.");
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = readFileSync(configPath, "utf-8");
|
||||
const config = JSON.parse(stripJsonComments(raw));
|
||||
|
||||
if (Array.isArray(config.plugin)) {
|
||||
config.plugin = config.plugin.filter((p: string) => p !== PLUGIN_NAME);
|
||||
if (config.plugin.length === 0) delete config.plugin;
|
||||
}
|
||||
|
||||
if (config.mcp?.mem0) {
|
||||
delete config.mcp.mem0;
|
||||
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
||||
}
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
||||
console.log(` Removed plugin + MCP from ${configPath}`);
|
||||
|
||||
const skillCount = uninstallSkills();
|
||||
if (skillCount > 0) {
|
||||
console.log(` Removed ${skillCount} slash commands from ~/.config/opencode/skills/`);
|
||||
}
|
||||
|
||||
console.log("Restart OpenCode to complete removal.");
|
||||
}
|
||||
|
||||
const cmd = process.argv[2];
|
||||
if (cmd === "uninstall" || cmd === "remove") {
|
||||
uninstall();
|
||||
} else {
|
||||
install();
|
||||
}
|
||||
Reference in New Issue
Block a user