135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
import fsp from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
|
|
import { SUPPORTED_AGENTS } from './registry.js';
|
|
|
|
const AGENT_KEY = 'opencode';
|
|
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
|
|
|
function resolveConfigDir(): string {
|
|
return path.join(os.homedir(), entry.configDir);
|
|
}
|
|
|
|
function resolveConfigPath(): string {
|
|
return path.join(resolveConfigDir(), entry.configFile);
|
|
}
|
|
|
|
function generatePluginScript(hookScriptPath: string, guardScriptPath: string): string {
|
|
return `// Elydora Audit Plugin for OpenCode
|
|
// Generated by @elydora/sdk — DO NOT EDIT
|
|
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
|
|
const HOOK_SCRIPT_PATH = ${JSON.stringify(hookScriptPath)};
|
|
const GUARD_SCRIPT_PATH = ${JSON.stringify(guardScriptPath)};
|
|
|
|
export const ElydoraAuditPlugin = async (ctx) => {
|
|
return {
|
|
"tool.execute.before": async () => {
|
|
// Guard — blocks tool if agent is frozen
|
|
let result;
|
|
try {
|
|
result = spawnSync('node', [GUARD_SCRIPT_PATH], {
|
|
timeout: 5000,
|
|
stdio: ['pipe', 'ignore', 'pipe'],
|
|
});
|
|
} catch (error) {
|
|
throw new Error('Elydora guard failed: ' + error.message);
|
|
}
|
|
|
|
if (result.error) {
|
|
throw new Error('Elydora guard failed: ' + result.error.message);
|
|
}
|
|
if (result.status !== 0) {
|
|
const message = result.stderr?.toString().trim() || 'Guard exited with status ' + result.status;
|
|
const prefix = result.status === 2 ? '' : 'Elydora guard failed: ';
|
|
throw new Error(prefix + message);
|
|
}
|
|
},
|
|
|
|
"tool.execute.after": async (input, output) => {
|
|
const data = JSON.stringify({
|
|
tool_name: input?.tool || 'unknown',
|
|
tool_input: input?.args || output?.args || {},
|
|
session_id: input?.sessionID || ctx?.project?.name || 'unknown',
|
|
});
|
|
const child = spawn('node', [HOOK_SCRIPT_PATH], {
|
|
detached: true,
|
|
stdio: ['pipe', 'ignore', 'ignore'],
|
|
});
|
|
child.on('error', (error) => {
|
|
console.error('[elydora] audit hook failed:', error.message);
|
|
});
|
|
child.stdin.on('error', (error) => {
|
|
console.error('[elydora] audit input failed:', error.message);
|
|
});
|
|
child.stdin.end(data);
|
|
child.unref();
|
|
},
|
|
};
|
|
};
|
|
`;
|
|
}
|
|
|
|
export const opencodePlugin: AgentPlugin = {
|
|
async install(config: InstallConfig): Promise<void> {
|
|
const configDir = resolveConfigDir();
|
|
await fsp.mkdir(configDir, { recursive: true });
|
|
|
|
const configPath = resolveConfigPath();
|
|
const script = generatePluginScript(config.hookScriptPath, config.guardScriptPath);
|
|
|
|
await fsp.writeFile(configPath, script, 'utf-8');
|
|
},
|
|
|
|
async uninstall(_agentId?: string): Promise<void> {
|
|
const configPath = resolveConfigPath();
|
|
try {
|
|
await fsp.unlink(configPath);
|
|
} catch {
|
|
// Already removed
|
|
}
|
|
},
|
|
|
|
async status(): Promise<PluginStatus> {
|
|
const configPath = resolveConfigPath();
|
|
|
|
let hookConfigured = false;
|
|
let hookScriptPath = '';
|
|
let guardScriptPath = '';
|
|
try {
|
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
|
const hookMatch = raw.match(/const HOOK_SCRIPT_PATH = ("(?:\\.|[^"\\])*");/);
|
|
const guardMatch = raw.match(/const GUARD_SCRIPT_PATH = ("(?:\\.|[^"\\])*");/);
|
|
if (hookMatch && guardMatch) {
|
|
hookScriptPath = JSON.parse(hookMatch[1]);
|
|
guardScriptPath = JSON.parse(guardMatch[1]);
|
|
hookConfigured = true;
|
|
}
|
|
} catch {
|
|
// File doesn't exist
|
|
}
|
|
|
|
let hookScriptExists = false;
|
|
if (hookScriptPath && guardScriptPath) {
|
|
try {
|
|
await fsp.access(hookScriptPath);
|
|
await fsp.access(guardScriptPath);
|
|
hookScriptExists = true;
|
|
} catch {
|
|
// File doesn't exist
|
|
}
|
|
}
|
|
|
|
return {
|
|
installed: hookConfigured && hookScriptExists,
|
|
agentName: AGENT_KEY,
|
|
displayName: entry.name,
|
|
hookConfigured,
|
|
hookScriptExists,
|
|
configPath,
|
|
};
|
|
},
|
|
};
|