import { tool } from "@opencode-ai/plugin"; import { readFile, writeFile, mkdir } from "fs/promises"; import { resolve, normalize, join, dirname } from "path"; import { homedir } from "os"; const CONFIG_BASE = resolve(homedir(), ".config/opencode"); function resolvePath(filePath) { if (!filePath.startsWith('/') && !filePath.startsWith('~')) { filePath = join(CONFIG_BASE, filePath); } if (filePath.startsWith('~/')) { filePath = filePath.replace('~/', homedir() + '/'); } const normalized = normalize(resolve(filePath)); if (!normalized.startsWith(CONFIG_BASE)) { throw new Error( `Access denied: Path must be within ~/.config/opencode\n` + `Attempted: ${normalized}\n` + `Use config_read/config_write/config_edit ONLY for opencode config files.` ); } return normalized; } export const FileProxyPlugin = async () => { return { tool: { config_read: tool({ description: `Read files from OpenCode config directory (~/.config/opencode). **REQUIRED when reading config files from outside ~/.config/opencode directory.** Use this tool when: - Reading agent definitions (agent/*.md) - Reading skills (skills/*/SKILL.md) - Reading workflows (OPTIMIZATION_WORKFLOW.md, etc.) - Current working directory is NOT ~/.config/opencode Do NOT use if already in ~/.config/opencode - use standard 'read' tool instead.`, args: { filePath: tool.schema.string() .describe("Path to file (e.g., 'agent/optimize.md' or '~/.config/opencode/skills/skill-name/SKILL.md')") }, async execute(args) { try { const validPath = resolvePath(args.filePath); const content = await readFile(validPath, "utf-8"); return content; } catch (error) { if (error.code === 'ENOENT') { return `❌ File not found: ${args.filePath}\nCheck path and try again.`; } if (error.code === 'EACCES') { return `❌ Permission denied: ${args.filePath}`; } if (error.message.includes('Access denied')) { return `❌ ${error.message}`; } return `❌ Error reading file: ${error.message}`; } } }), config_write: tool({ description: `Write/create files in OpenCode config directory (~/.config/opencode). **REQUIRED when creating/writing config files from outside ~/.config/opencode directory.** Use this tool when: - Creating new skills (skills/new-skill/SKILL.md) - Creating new agent definitions - Writing workflow documentation - Current working directory is NOT ~/.config/opencode Common use case: Optimize agent creating skills or updating workflows from project directories. Do NOT use if already in ~/.config/opencode - use standard 'write' tool instead.`, args: { filePath: tool.schema.string() .describe("Path to file (e.g., 'skills/my-skill/SKILL.md')"), content: tool.schema.string() .describe("Complete file content to write") }, async execute(args) { try { const validPath = resolvePath(args.filePath); await mkdir(dirname(validPath), { recursive: true }); await writeFile(validPath, args.content, "utf-8"); return `✅ Successfully wrote to ${args.filePath}`; } catch (error) { if (error.code === 'EACCES') { return `❌ Permission denied: ${args.filePath}`; } if (error.message.includes('Access denied')) { return `❌ ${error.message}`; } return `❌ Error writing file: ${error.message}`; } } }), config_edit: tool({ description: `Edit existing files in OpenCode config directory (~/.config/opencode). **REQUIRED when editing config files from outside ~/.config/opencode directory.** Use this tool when: - Updating agent definitions (adding sections to optimize.md) - Enhancing existing skills - Modifying workflow docs - Current working directory is NOT ~/.config/opencode Operations: append (add to end), prepend (add to beginning), replace (find and replace text). Do NOT use if already in ~/.config/opencode - use standard 'edit' tool instead.`, args: { filePath: tool.schema.string() .describe("Path to file to edit"), operation: tool.schema.enum(["append", "prepend", "replace"]) .describe("Edit operation to perform"), content: tool.schema.string() .describe("Content to add or replacement text"), searchPattern: tool.schema.string() .optional() .describe("Regex pattern to find (required for 'replace' operation)") }, async execute(args) { try { const validPath = resolvePath(args.filePath); let fileContent = await readFile(validPath, "utf-8"); switch (args.operation) { case "append": fileContent += "\n" + args.content; break; case "prepend": fileContent = args.content + "\n" + fileContent; break; case "replace": if (!args.searchPattern) { throw new Error("searchPattern required for replace operation"); } fileContent = fileContent.replace(new RegExp(args.searchPattern, "g"), args.content); break; } await writeFile(validPath, fileContent, "utf-8"); return `✅ Successfully edited ${args.filePath} (${args.operation})`; } catch (error) { if (error.code === 'ENOENT') { return `❌ File not found: ${args.filePath}\nUse config_write to create new files.`; } if (error.code === 'EACCES') { return `❌ Permission denied: ${args.filePath}`; } if (error.message.includes('Access denied')) { return `❌ ${error.message}`; } return `❌ Error editing file: ${error.message}`; } } }) } }; };