nixos/shared/linked-dotfiles/opencode/plugin/file-proxy.js

177 lines
6.2 KiB
JavaScript

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}`;
}
}
})
}
};
};