Migrate to per-agent subdirectories, uniform 8-agent support, cross-platform fixes
- All paths use ~/.elydora/{agentId}/ per-agent isolation
- Add copilot, letta plugins; split kiro into kirocli + kiroide
- Remove augment (SDK-only agent, not one-click)
- Fix chmod to skip on Windows (platform check)
- Fix uninstall to filter by specific agentId
- Uniform 8 agents: claudecode, cursor, gemini, kirocli, kiroide, opencode, copilot, letta
- Update README agent table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb7a6320d5
commit
9bb819b182
13 files changed
+729
-124
No files matched your search
@@ -68,9 +68,11 @@ npx elydora install \
|
|||||||
| Claude Code | `claudecode` |
|
| Claude Code | `claudecode` |
|
||||||
| Cursor | `cursor` |
|
| Cursor | `cursor` |
|
||||||
| Gemini CLI | `gemini` |
|
| Gemini CLI | `gemini` |
|
||||||
| Augment Code | `augment` |
|
| Kiro CLI | `kirocli` |
|
||||||
| Kiro | `kiro` |
|
| Kiro IDE | `kiroide` |
|
||||||
| OpenCode | `opencode` |
|
| OpenCode | `opencode` |
|
||||||
|
| Copilot CLI | `copilot` |
|
||||||
|
| Letta Code | `letta` |
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
|
|||||||
+91
-32
@@ -9,9 +9,11 @@ import { generateHookScript, generateGuardScript } from './plugins/hook-template
|
|||||||
import { claudecodePlugin } from './plugins/claudecode.js';
|
import { claudecodePlugin } from './plugins/claudecode.js';
|
||||||
import { cursorPlugin } from './plugins/cursor.js';
|
import { cursorPlugin } from './plugins/cursor.js';
|
||||||
import { geminiPlugin } from './plugins/gemini.js';
|
import { geminiPlugin } from './plugins/gemini.js';
|
||||||
import { augmentPlugin } from './plugins/augment.js';
|
import { kirocliPlugin } from './plugins/kirocli.js';
|
||||||
import { kiroPlugin } from './plugins/kiro.js';
|
import { kiroidePlugin } from './plugins/kiroide.js';
|
||||||
import { opencodePlugin } from './plugins/opencode.js';
|
import { opencodePlugin } from './plugins/opencode.js';
|
||||||
|
import { copilotPlugin } from './plugins/copilot.js';
|
||||||
|
import { lettaPlugin } from './plugins/letta.js';
|
||||||
|
|
||||||
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
||||||
|
|
||||||
@@ -19,9 +21,11 @@ const PLUGINS: ReadonlyMap<string, AgentPlugin> = new Map([
|
|||||||
['claudecode', claudecodePlugin],
|
['claudecode', claudecodePlugin],
|
||||||
['cursor', cursorPlugin],
|
['cursor', cursorPlugin],
|
||||||
['gemini', geminiPlugin],
|
['gemini', geminiPlugin],
|
||||||
['augment', augmentPlugin],
|
['kirocli', kirocliPlugin],
|
||||||
['kiro', kiroPlugin],
|
['kiroide', kiroidePlugin],
|
||||||
['opencode', opencodePlugin],
|
['opencode', opencodePlugin],
|
||||||
|
['copilot', copilotPlugin],
|
||||||
|
['letta', lettaPlugin],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -38,7 +42,7 @@ function printUsage(): void {
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
elydora install --agent <name> --org_id <id> --agent_id <id> --private_key <key> --kid <kid> [--token <jwt>] [--base_url <url>]
|
elydora install --agent <name> --org_id <id> --agent_id <id> --private_key <key> --kid <kid> [--token <jwt>] [--base_url <url>]
|
||||||
elydora uninstall --agent <name>
|
elydora uninstall --agent <name> [--agent_id <id>]
|
||||||
elydora status
|
elydora status
|
||||||
elydora agents
|
elydora agents
|
||||||
|
|
||||||
@@ -102,40 +106,43 @@ async function cmdInstall(args: string[]): Promise<void> {
|
|||||||
|
|
||||||
console.log(`Verifying private key... Public key: ${publicKey.slice(0, 12)}...`);
|
console.log(`Verifying private key... Public key: ${publicKey.slice(0, 12)}...`);
|
||||||
|
|
||||||
// Create ~/.elydora directory structure
|
// Create ~/.elydora/{agentId}/ directory
|
||||||
const agentsDir = path.join(ELYDORA_DIR, 'agents');
|
const agentDir = path.join(ELYDORA_DIR, agentId);
|
||||||
const hooksDir = path.join(ELYDORA_DIR, 'hooks');
|
await fsp.mkdir(agentDir, { recursive: true });
|
||||||
await fsp.mkdir(agentsDir, { recursive: true });
|
|
||||||
await fsp.mkdir(hooksDir, { recursive: true });
|
|
||||||
|
|
||||||
// Write agent config
|
// Write agent config
|
||||||
const agentConfigPath = path.join(agentsDir, `${agentName}.json`);
|
const agentConfigPath = path.join(agentDir, 'config.json');
|
||||||
const agentConfig = {
|
const agentConfig = {
|
||||||
org_id: orgId,
|
org_id: orgId,
|
||||||
agent_id: agentId,
|
agent_id: agentId,
|
||||||
kid,
|
kid,
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
...(token ? { token } : {}),
|
...(token ? { token } : {}),
|
||||||
|
agent_name: agentName,
|
||||||
};
|
};
|
||||||
await fsp.writeFile(agentConfigPath, JSON.stringify(agentConfig, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(agentConfigPath, JSON.stringify(agentConfig, null, 2) + '\n', 'utf-8');
|
||||||
await fsp.chmod(agentConfigPath, 0o600);
|
if (process.platform !== 'win32') {
|
||||||
|
await fsp.chmod(agentConfigPath, 0o600);
|
||||||
|
}
|
||||||
console.log(` Agent config: ${agentConfigPath}`);
|
console.log(` Agent config: ${agentConfigPath}`);
|
||||||
|
|
||||||
// Write private key (chmod 600)
|
// Write private key (chmod 600)
|
||||||
const keyPath = path.join(agentsDir, `${agentName}.key`);
|
const keyPath = path.join(agentDir, 'private.key');
|
||||||
await fsp.writeFile(keyPath, privateKey, { encoding: 'utf-8', mode: 0o600 });
|
await fsp.writeFile(keyPath, privateKey, { encoding: 'utf-8', mode: 0o600 });
|
||||||
await fsp.chmod(keyPath, 0o600);
|
if (process.platform !== 'win32') {
|
||||||
|
await fsp.chmod(keyPath, 0o600);
|
||||||
|
}
|
||||||
console.log(` Private key: ${keyPath}`);
|
console.log(` Private key: ${keyPath}`);
|
||||||
|
|
||||||
// Generate and write hook script (PostToolUse — audit logging)
|
// Generate and write hook script (PostToolUse — audit logging)
|
||||||
const hookScriptPath = path.join(hooksDir, `${agentName}-hook.js`);
|
const hookScriptPath = path.join(agentDir, 'hook.js');
|
||||||
const hookScript = generateHookScript(agentName);
|
const hookScript = generateHookScript(agentName, agentId);
|
||||||
await fsp.writeFile(hookScriptPath, hookScript, { encoding: 'utf-8', mode: 0o755 });
|
await fsp.writeFile(hookScriptPath, hookScript, { encoding: 'utf-8', mode: 0o755 });
|
||||||
console.log(` Hook script: ${hookScriptPath}`);
|
console.log(` Hook script: ${hookScriptPath}`);
|
||||||
|
|
||||||
// Generate and write guard script (PreToolUse — freeze enforcement)
|
// Generate and write guard script (PreToolUse — freeze enforcement)
|
||||||
const guardScriptPath = path.join(hooksDir, `${agentName}-guard.js`);
|
const guardScriptPath = path.join(agentDir, 'guard.js');
|
||||||
const guardScript = generateGuardScript(agentName);
|
const guardScript = generateGuardScript(agentName, agentId);
|
||||||
await fsp.writeFile(guardScriptPath, guardScript, { encoding: 'utf-8', mode: 0o755 });
|
await fsp.writeFile(guardScriptPath, guardScript, { encoding: 'utf-8', mode: 0o755 });
|
||||||
console.log(` Guard script: ${guardScriptPath}`);
|
console.log(` Guard script: ${guardScriptPath}`);
|
||||||
|
|
||||||
@@ -164,6 +171,7 @@ async function cmdUninstall(args: string[]): Promise<void> {
|
|||||||
args,
|
args,
|
||||||
options: {
|
options: {
|
||||||
agent: { type: 'string' },
|
agent: { type: 'string' },
|
||||||
|
agent_id: { type: 'string' },
|
||||||
},
|
},
|
||||||
strict: true,
|
strict: true,
|
||||||
});
|
});
|
||||||
@@ -174,25 +182,49 @@ async function cmdUninstall(args: string[]): Promise<void> {
|
|||||||
die(`Unknown agent "${agentName}". Supported: ${Array.from(SUPPORTED_AGENTS.keys()).join(', ')}`);
|
die(`Unknown agent "${agentName}". Supported: ${Array.from(SUPPORTED_AGENTS.keys()).join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let agentId = values.agent_id;
|
||||||
|
|
||||||
|
// If --agent_id not provided, scan ~/.elydora/*/config.json for matching agent_name
|
||||||
|
if (!agentId) {
|
||||||
|
try {
|
||||||
|
const entries = await fsp.readdir(ELYDORA_DIR, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const cfgPath = path.join(ELYDORA_DIR, entry.name, 'config.json');
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(cfgPath, 'utf-8');
|
||||||
|
const cfg = JSON.parse(raw);
|
||||||
|
if (cfg.agent_name === agentName) {
|
||||||
|
agentId = entry.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable configs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ELYDORA_DIR may not exist
|
||||||
|
}
|
||||||
|
if (!agentId) {
|
||||||
|
die(`No installed agent found for "${agentName}". Use --agent_id to specify explicitly.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const plugin = PLUGINS.get(agentName)!;
|
const plugin = PLUGINS.get(agentName)!;
|
||||||
const entry = SUPPORTED_AGENTS.get(agentName)!;
|
const registryEntry = SUPPORTED_AGENTS.get(agentName)!;
|
||||||
|
|
||||||
// Uninstall agent-specific config
|
// Uninstall agent-specific config
|
||||||
await plugin.uninstall();
|
await plugin.uninstall(agentId);
|
||||||
|
|
||||||
// Remove hook and guard scripts
|
// Remove entire agent directory
|
||||||
const hookScriptPath = path.join(ELYDORA_DIR, 'hooks', `${agentName}-hook.js`);
|
const agentDir = path.join(ELYDORA_DIR, agentId);
|
||||||
const guardScriptPath = path.join(ELYDORA_DIR, 'hooks', `${agentName}-guard.js`);
|
try {
|
||||||
try { await fsp.unlink(hookScriptPath); } catch { /* Already removed */ }
|
await fsp.rm(agentDir, { recursive: true, force: true });
|
||||||
try { await fsp.unlink(guardScriptPath); } catch { /* Already removed */ }
|
} catch {
|
||||||
|
// Already removed
|
||||||
|
}
|
||||||
|
|
||||||
// Remove agent config and key
|
console.log(`Elydora audit hook uninstalled for ${registryEntry.name}.`);
|
||||||
const agentConfigPath = path.join(ELYDORA_DIR, 'agents', `${agentName}.json`);
|
|
||||||
const keyPath = path.join(ELYDORA_DIR, 'agents', `${agentName}.key`);
|
|
||||||
try { await fsp.unlink(agentConfigPath); } catch { /* */ }
|
|
||||||
try { await fsp.unlink(keyPath); } catch { /* */ }
|
|
||||||
|
|
||||||
console.log(`Elydora audit hook uninstalled for ${entry.name}.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdStatus(): Promise<void> {
|
async function cmdStatus(): Promise<void> {
|
||||||
@@ -200,15 +232,42 @@ async function cmdStatus(): Promise<void> {
|
|||||||
|
|
||||||
let anyInstalled = false;
|
let anyInstalled = false;
|
||||||
|
|
||||||
|
// Scan ~/.elydora/*/config.json to discover installed agents
|
||||||
|
const installedAgents: Array<{ agentId: string; agentName: string; configPath: string }> = [];
|
||||||
|
try {
|
||||||
|
const entries = await fsp.readdir(ELYDORA_DIR, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const cfgPath = path.join(ELYDORA_DIR, entry.name, 'config.json');
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(cfgPath, 'utf-8');
|
||||||
|
const cfg = JSON.parse(raw);
|
||||||
|
if (cfg.agent_name && cfg.agent_id) {
|
||||||
|
installedAgents.push({ agentId: entry.name, agentName: cfg.agent_name, configPath: cfgPath });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable configs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ELYDORA_DIR may not exist
|
||||||
|
}
|
||||||
|
|
||||||
for (const [name, plugin] of PLUGINS) {
|
for (const [name, plugin] of PLUGINS) {
|
||||||
const st = await plugin.status();
|
const st = await plugin.status();
|
||||||
const statusIcon = st.installed ? '[installed]' : '[not installed]';
|
const statusIcon = st.installed ? '[installed]' : '[not installed]';
|
||||||
|
|
||||||
|
// Find matching installed agent(s) for this plugin
|
||||||
|
const matching = installedAgents.filter((a) => a.agentName === name);
|
||||||
|
|
||||||
console.log(` ${st.displayName} (${name}) ${statusIcon}`);
|
console.log(` ${st.displayName} (${name}) ${statusIcon}`);
|
||||||
if (st.installed || st.hookConfigured || st.hookScriptExists) {
|
if (st.installed || st.hookConfigured || st.hookScriptExists) {
|
||||||
console.log(` Hook config: ${st.hookConfigured ? 'yes' : 'no'}`);
|
console.log(` Hook config: ${st.hookConfigured ? 'yes' : 'no'}`);
|
||||||
console.log(` Hook script: ${st.hookScriptExists ? 'yes' : 'no'}`);
|
console.log(` Hook script: ${st.hookScriptExists ? 'yes' : 'no'}`);
|
||||||
console.log(` Config path: ${st.configPath}`);
|
console.log(` Config path: ${st.configPath}`);
|
||||||
|
for (const m of matching) {
|
||||||
|
console.log(` Agent ID: ${m.agentId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (st.installed) anyInstalled = true;
|
if (st.installed) anyInstalled = true;
|
||||||
|
|||||||
+1
-1
@@ -21,6 +21,6 @@ export interface PluginStatus {
|
|||||||
|
|
||||||
export interface AgentPlugin {
|
export interface AgentPlugin {
|
||||||
install(config: InstallConfig): Promise<void>;
|
install(config: InstallConfig): Promise<void>;
|
||||||
uninstall(): Promise<void>;
|
uninstall(agentId?: string): Promise<void>;
|
||||||
status(): Promise<PluginStatus>;
|
status(): Promise<PluginStatus>;
|
||||||
}
|
}
|
||||||
+36
-12
@@ -19,14 +19,22 @@ function buildHookCommand(scriptPath: string): string {
|
|||||||
return `node "${scriptPath}"`;
|
return `node "${scriptPath}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterElydoraEntries(arr: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
function isElydoraCommand(cmd: string, agentId?: string): boolean {
|
||||||
|
if (!cmd.includes('elydora')) return false;
|
||||||
|
if (agentId) {
|
||||||
|
return cmd.includes(agentId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterElydoraEntries(arr: Array<Record<string, unknown>>, agentId?: string): Array<Record<string, unknown>> {
|
||||||
return arr.filter((entry) => {
|
return arr.filter((entry) => {
|
||||||
if (Array.isArray(entry.hooks)) {
|
if (Array.isArray(entry.hooks)) {
|
||||||
const cmds = entry.hooks as Array<Record<string, unknown>>;
|
const cmds = entry.hooks as Array<Record<string, unknown>>;
|
||||||
return !cmds.some((h) => typeof h.command === 'string' && h.command.includes('elydora'));
|
return !cmds.some((h) => typeof h.command === 'string' && isElydoraCommand(h.command, agentId));
|
||||||
}
|
}
|
||||||
if (typeof entry.command === 'string') {
|
if (typeof entry.command === 'string') {
|
||||||
return !entry.command.includes('elydora');
|
return !isElydoraCommand(entry.command, agentId);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -76,7 +84,7 @@ export const claudecodePlugin: AgentPlugin = {
|
|||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
},
|
},
|
||||||
|
|
||||||
async uninstall(): Promise<void> {
|
async uninstall(agentId?: string): Promise<void> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
let settings: Record<string, unknown>;
|
let settings: Record<string, unknown>;
|
||||||
@@ -91,10 +99,10 @@ export const claudecodePlugin: AgentPlugin = {
|
|||||||
if (!hooks) return;
|
if (!hooks) return;
|
||||||
|
|
||||||
if (Array.isArray(hooks.PreToolUse)) {
|
if (Array.isArray(hooks.PreToolUse)) {
|
||||||
hooks.PreToolUse = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>);
|
hooks.PreToolUse = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
if (Array.isArray(hooks.PostToolUse)) {
|
if (Array.isArray(hooks.PostToolUse)) {
|
||||||
hooks.PostToolUse = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>);
|
hooks.PostToolUse = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
@@ -102,9 +110,9 @@ export const claudecodePlugin: AgentPlugin = {
|
|||||||
|
|
||||||
async status(): Promise<PluginStatus> {
|
async status(): Promise<PluginStatus> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
const hookScriptPath = path.join(os.homedir(), '.elydora', 'hooks', `${AGENT_KEY}-hook.js`);
|
|
||||||
|
|
||||||
let hookConfigured = false;
|
let hookConfigured = false;
|
||||||
|
let hookScriptPath = '';
|
||||||
try {
|
try {
|
||||||
const raw = await fsp.readFile(configPath, 'utf-8');
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
const settings = JSON.parse(raw);
|
const settings = JSON.parse(raw);
|
||||||
@@ -122,17 +130,33 @@ export const claudecodePlugin: AgentPlugin = {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
hookConfigured = checkArr(hooks.PreToolUse) && checkArr(hooks.PostToolUse);
|
hookConfigured = checkArr(hooks.PreToolUse) && checkArr(hooks.PostToolUse);
|
||||||
|
|
||||||
|
// Extract hook script path from PostToolUse command
|
||||||
|
if (hookConfigured && Array.isArray(hooks.PostToolUse)) {
|
||||||
|
for (const e of hooks.PostToolUse as Array<Record<string, unknown>>) {
|
||||||
|
if (Array.isArray(e.hooks)) {
|
||||||
|
for (const h of e.hooks as Array<Record<string, unknown>>) {
|
||||||
|
const cmd = h.command as string;
|
||||||
|
if (cmd && cmd.includes('elydora')) {
|
||||||
|
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Config not readable
|
// Config not readable
|
||||||
}
|
}
|
||||||
|
|
||||||
let hookScriptExists = false;
|
let hookScriptExists = false;
|
||||||
try {
|
if (hookScriptPath) {
|
||||||
await fsp.access(hookScriptPath);
|
try {
|
||||||
hookScriptExists = true;
|
await fsp.access(hookScriptPath);
|
||||||
} catch {
|
hookScriptExists = true;
|
||||||
// File doesn't exist
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import fsp from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
|
||||||
|
import { SUPPORTED_AGENTS } from './registry.js';
|
||||||
|
|
||||||
|
const AGENT_KEY = 'copilot';
|
||||||
|
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
||||||
|
|
||||||
|
function resolveConfigDir(): string {
|
||||||
|
return path.join(process.cwd(), entry.configDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveConfigPath(): string {
|
||||||
|
return path.join(resolveConfigDir(), entry.configFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHookCommand(scriptPath: string): string {
|
||||||
|
return `node "${scriptPath}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isElydoraCommand(cmd: string, agentId?: string): boolean {
|
||||||
|
if (!cmd.includes('elydora')) return false;
|
||||||
|
if (agentId) {
|
||||||
|
return cmd.includes(agentId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterElydoraEntries(arr: Array<Record<string, unknown>>, agentId?: string): Array<Record<string, unknown>> {
|
||||||
|
return arr.filter((h) => {
|
||||||
|
if (typeof h.bash === 'string' && isElydoraCommand(h.bash, agentId)) return false;
|
||||||
|
if (typeof h.powershell === 'string' && isElydoraCommand(h.powershell, agentId)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const copilotPlugin: AgentPlugin = {
|
||||||
|
async install(config: InstallConfig): Promise<void> {
|
||||||
|
const configDir = resolveConfigDir();
|
||||||
|
await fsp.mkdir(configDir, { recursive: true });
|
||||||
|
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
let settings: Record<string, unknown> = { version: 1 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
|
settings = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
// Start fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.version = 1;
|
||||||
|
|
||||||
|
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
||||||
|
settings.hooks = {};
|
||||||
|
}
|
||||||
|
const hooks = settings.hooks as Record<string, unknown>;
|
||||||
|
|
||||||
|
// --- preToolUse (guard — freeze enforcement) ---
|
||||||
|
if (!Array.isArray(hooks.preToolUse)) {
|
||||||
|
hooks.preToolUse = [];
|
||||||
|
}
|
||||||
|
const preFiltered = filterElydoraEntries(hooks.preToolUse as Array<Record<string, unknown>>);
|
||||||
|
preFiltered.push({
|
||||||
|
type: 'command',
|
||||||
|
bash: buildHookCommand(config.guardScriptPath),
|
||||||
|
powershell: buildHookCommand(config.guardScriptPath),
|
||||||
|
timeoutSec: 5,
|
||||||
|
});
|
||||||
|
hooks.preToolUse = preFiltered;
|
||||||
|
|
||||||
|
// --- postToolUse (audit logging) ---
|
||||||
|
if (!Array.isArray(hooks.postToolUse)) {
|
||||||
|
hooks.postToolUse = [];
|
||||||
|
}
|
||||||
|
const postFiltered = filterElydoraEntries(hooks.postToolUse as Array<Record<string, unknown>>);
|
||||||
|
postFiltered.push({
|
||||||
|
type: 'command',
|
||||||
|
bash: buildHookCommand(config.hookScriptPath),
|
||||||
|
powershell: buildHookCommand(config.hookScriptPath),
|
||||||
|
timeoutSec: 5,
|
||||||
|
});
|
||||||
|
hooks.postToolUse = postFiltered;
|
||||||
|
|
||||||
|
settings.hooks = hooks;
|
||||||
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
|
},
|
||||||
|
|
||||||
|
async uninstall(agentId?: string): Promise<void> {
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
|
let settings: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
|
settings = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks = settings.hooks as Record<string, unknown> | undefined;
|
||||||
|
if (!hooks) return;
|
||||||
|
|
||||||
|
if (Array.isArray(hooks.preToolUse)) {
|
||||||
|
hooks.preToolUse = filterElydoraEntries(hooks.preToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
|
}
|
||||||
|
if (Array.isArray(hooks.postToolUse)) {
|
||||||
|
hooks.postToolUse = filterElydoraEntries(hooks.postToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
|
},
|
||||||
|
|
||||||
|
async status(): Promise<PluginStatus> {
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
|
let hookConfigured = false;
|
||||||
|
let hookScriptPath = '';
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
|
const settings = JSON.parse(raw);
|
||||||
|
const hooks = settings.hooks as Record<string, unknown> | undefined;
|
||||||
|
if (hooks) {
|
||||||
|
const check = (arr: unknown) =>
|
||||||
|
Array.isArray(arr) && (arr as Array<Record<string, unknown>>).some(
|
||||||
|
(h) =>
|
||||||
|
(typeof h.bash === 'string' && h.bash.includes('elydora')) ||
|
||||||
|
(typeof h.powershell === 'string' && h.powershell.includes('elydora')),
|
||||||
|
);
|
||||||
|
hookConfigured = check(hooks.preToolUse) && check(hooks.postToolUse);
|
||||||
|
|
||||||
|
// Extract hook script path from postToolUse bash/powershell command
|
||||||
|
if (hookConfigured && Array.isArray(hooks.postToolUse)) {
|
||||||
|
for (const h of hooks.postToolUse as Array<Record<string, unknown>>) {
|
||||||
|
const cmd = (h.bash || h.powershell) as string;
|
||||||
|
if (cmd && cmd.includes('elydora')) {
|
||||||
|
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Config not readable
|
||||||
|
}
|
||||||
|
|
||||||
|
let hookScriptExists = false;
|
||||||
|
if (hookScriptPath) {
|
||||||
|
try {
|
||||||
|
await fsp.access(hookScriptPath);
|
||||||
|
hookScriptExists = true;
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: hookConfigured && hookScriptExists,
|
||||||
|
agentName: AGENT_KEY,
|
||||||
|
displayName: entry.name,
|
||||||
|
hookConfigured,
|
||||||
|
hookScriptExists,
|
||||||
|
configPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
+32
-11
@@ -20,9 +20,17 @@ function buildHookCommand(scriptPath: string): string {
|
|||||||
return `node "${scriptPath}"`;
|
return `node "${scriptPath}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterElydoraEntries(arr: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
function isElydoraCommand(cmd: string, agentId?: string): boolean {
|
||||||
|
if (!cmd.includes('elydora')) return false;
|
||||||
|
if (agentId) {
|
||||||
|
return cmd.includes(agentId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterElydoraEntries(arr: Array<Record<string, unknown>>, agentId?: string): Array<Record<string, unknown>> {
|
||||||
return arr.filter(
|
return arr.filter(
|
||||||
(h) => typeof h.command === 'string' && !h.command.includes('elydora'),
|
(h) => typeof h.command === 'string' && !isElydoraCommand(h.command, agentId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +71,11 @@ export const cursorPlugin: AgentPlugin = {
|
|||||||
hooks.postToolUse = postFiltered;
|
hooks.postToolUse = postFiltered;
|
||||||
|
|
||||||
settings.hooks = hooks;
|
settings.hooks = hooks;
|
||||||
|
settings.version = 1;
|
||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
},
|
},
|
||||||
|
|
||||||
async uninstall(): Promise<void> {
|
async uninstall(agentId?: string): Promise<void> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
let settings: Record<string, unknown>;
|
let settings: Record<string, unknown>;
|
||||||
@@ -81,10 +90,10 @@ export const cursorPlugin: AgentPlugin = {
|
|||||||
if (!hooks) return;
|
if (!hooks) return;
|
||||||
|
|
||||||
if (Array.isArray(hooks.preToolUse)) {
|
if (Array.isArray(hooks.preToolUse)) {
|
||||||
hooks.preToolUse = filterElydoraEntries(hooks.preToolUse as Array<Record<string, unknown>>);
|
hooks.preToolUse = filterElydoraEntries(hooks.preToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
if (Array.isArray(hooks.postToolUse)) {
|
if (Array.isArray(hooks.postToolUse)) {
|
||||||
hooks.postToolUse = filterElydoraEntries(hooks.postToolUse as Array<Record<string, unknown>>);
|
hooks.postToolUse = filterElydoraEntries(hooks.postToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
@@ -92,9 +101,9 @@ export const cursorPlugin: AgentPlugin = {
|
|||||||
|
|
||||||
async status(): Promise<PluginStatus> {
|
async status(): Promise<PluginStatus> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
const hookScriptPath = path.join(os.homedir(), '.elydora', 'hooks', `${AGENT_KEY}-hook.js`);
|
|
||||||
|
|
||||||
let hookConfigured = false;
|
let hookConfigured = false;
|
||||||
|
let hookScriptPath = '';
|
||||||
try {
|
try {
|
||||||
const raw = await fsp.readFile(configPath, 'utf-8');
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
const settings = JSON.parse(raw);
|
const settings = JSON.parse(raw);
|
||||||
@@ -105,17 +114,29 @@ export const cursorPlugin: AgentPlugin = {
|
|||||||
(h) => typeof h.command === 'string' && h.command.includes('elydora'),
|
(h) => typeof h.command === 'string' && h.command.includes('elydora'),
|
||||||
);
|
);
|
||||||
hookConfigured = check(hooks.preToolUse) && check(hooks.postToolUse);
|
hookConfigured = check(hooks.preToolUse) && check(hooks.postToolUse);
|
||||||
|
|
||||||
|
// Extract hook script path from postToolUse command
|
||||||
|
if (hookConfigured && Array.isArray(hooks.postToolUse)) {
|
||||||
|
for (const h of hooks.postToolUse as Array<Record<string, unknown>>) {
|
||||||
|
const cmd = h.command as string;
|
||||||
|
if (cmd && cmd.includes('elydora')) {
|
||||||
|
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Config not readable
|
// Config not readable
|
||||||
}
|
}
|
||||||
|
|
||||||
let hookScriptExists = false;
|
let hookScriptExists = false;
|
||||||
try {
|
if (hookScriptPath) {
|
||||||
await fsp.access(hookScriptPath);
|
try {
|
||||||
hookScriptExists = true;
|
await fsp.access(hookScriptPath);
|
||||||
} catch {
|
hookScriptExists = true;
|
||||||
// File doesn't exist
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+36
-12
@@ -19,13 +19,21 @@ function buildHookCommand(scriptPath: string): string {
|
|||||||
return `node "${scriptPath}"`;
|
return `node "${scriptPath}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterElydoraEntries(arr: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
function isElydoraCommand(cmd: string, agentId?: string): boolean {
|
||||||
|
if (!cmd.includes('elydora')) return false;
|
||||||
|
if (agentId) {
|
||||||
|
return cmd.includes(agentId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterElydoraEntries(arr: Array<Record<string, unknown>>, agentId?: string): Array<Record<string, unknown>> {
|
||||||
return arr.filter((entry) => {
|
return arr.filter((entry) => {
|
||||||
if (Array.isArray(entry.hooks)) {
|
if (Array.isArray(entry.hooks)) {
|
||||||
const cmds = entry.hooks as Array<Record<string, unknown>>;
|
const cmds = entry.hooks as Array<Record<string, unknown>>;
|
||||||
return !cmds.some((h) => typeof h.command === 'string' && h.command.includes('elydora'));
|
return !cmds.some((h) => typeof h.command === 'string' && isElydoraCommand(h.command, agentId));
|
||||||
}
|
}
|
||||||
if (typeof entry.command === 'string') return !entry.command.includes('elydora');
|
if (typeof entry.command === 'string') return !isElydoraCommand(entry.command, agentId);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -74,7 +82,7 @@ export const geminiPlugin: AgentPlugin = {
|
|||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
},
|
},
|
||||||
|
|
||||||
async uninstall(): Promise<void> {
|
async uninstall(agentId?: string): Promise<void> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
let settings: Record<string, unknown>;
|
let settings: Record<string, unknown>;
|
||||||
@@ -89,10 +97,10 @@ export const geminiPlugin: AgentPlugin = {
|
|||||||
if (!hooks) return;
|
if (!hooks) return;
|
||||||
|
|
||||||
if (Array.isArray(hooks.BeforeTool)) {
|
if (Array.isArray(hooks.BeforeTool)) {
|
||||||
hooks.BeforeTool = filterElydoraEntries(hooks.BeforeTool as Array<Record<string, unknown>>);
|
hooks.BeforeTool = filterElydoraEntries(hooks.BeforeTool as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
if (Array.isArray(hooks.AfterTool)) {
|
if (Array.isArray(hooks.AfterTool)) {
|
||||||
hooks.AfterTool = filterElydoraEntries(hooks.AfterTool as Array<Record<string, unknown>>);
|
hooks.AfterTool = filterElydoraEntries(hooks.AfterTool as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
@@ -100,9 +108,9 @@ export const geminiPlugin: AgentPlugin = {
|
|||||||
|
|
||||||
async status(): Promise<PluginStatus> {
|
async status(): Promise<PluginStatus> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
const hookScriptPath = path.join(os.homedir(), '.elydora', 'hooks', `${AGENT_KEY}-hook.js`);
|
|
||||||
|
|
||||||
let hookConfigured = false;
|
let hookConfigured = false;
|
||||||
|
let hookScriptPath = '';
|
||||||
try {
|
try {
|
||||||
const raw = await fsp.readFile(configPath, 'utf-8');
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
const settings = JSON.parse(raw);
|
const settings = JSON.parse(raw);
|
||||||
@@ -120,17 +128,33 @@ export const geminiPlugin: AgentPlugin = {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
hookConfigured = checkArr(hooks.BeforeTool) && checkArr(hooks.AfterTool);
|
hookConfigured = checkArr(hooks.BeforeTool) && checkArr(hooks.AfterTool);
|
||||||
|
|
||||||
|
// Extract hook script path from AfterTool command
|
||||||
|
if (hookConfigured && Array.isArray(hooks.AfterTool)) {
|
||||||
|
for (const e of hooks.AfterTool as Array<Record<string, unknown>>) {
|
||||||
|
if (Array.isArray(e.hooks)) {
|
||||||
|
for (const h of e.hooks as Array<Record<string, unknown>>) {
|
||||||
|
const cmd = h.command as string;
|
||||||
|
if (cmd && cmd.includes('elydora')) {
|
||||||
|
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Config not readable
|
// Config not readable
|
||||||
}
|
}
|
||||||
|
|
||||||
let hookScriptExists = false;
|
let hookScriptExists = false;
|
||||||
try {
|
if (hookScriptPath) {
|
||||||
await fsp.access(hookScriptPath);
|
try {
|
||||||
hookScriptExists = true;
|
await fsp.access(hookScriptPath);
|
||||||
} catch {
|
hookScriptExists = true;
|
||||||
// File doesn't exist
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* inline so it has zero runtime dependency on the SDK.
|
* inline so it has zero runtime dependency on the SDK.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function generateHookScript(agentName: string): string {
|
export function generateHookScript(agentName: string, agentId: string): string {
|
||||||
return `#!/usr/bin/env node
|
return `#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -17,11 +17,12 @@ const fs = require('node:fs');
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const AGENT_NAME = ${JSON.stringify(agentName)};
|
const AGENT_NAME = ${JSON.stringify(agentName)};
|
||||||
|
const AGENT_ID = ${JSON.stringify(agentId)};
|
||||||
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
||||||
const CONFIG_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.json');
|
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
|
||||||
const KEY_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.key');
|
const KEY_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'private.key');
|
||||||
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, 'chain-state.json');
|
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'chain-state.json');
|
||||||
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, 'error.log');
|
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'error.log');
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Embedded crypto (from @elydora/sdk src/crypto.ts + src/utils.ts)
|
// Embedded crypto (from @elydora/sdk src/crypto.ts + src/utils.ts)
|
||||||
@@ -302,7 +303,7 @@ main();
|
|||||||
* - Fails open: if the API is unreachable, tool execution is allowed
|
* - Fails open: if the API is unreachable, tool execution is allowed
|
||||||
* - If frozen: exits with code 1 to block the tool
|
* - If frozen: exits with code 1 to block the tool
|
||||||
*/
|
*/
|
||||||
export function generateGuardScript(agentName: string): string {
|
export function generateGuardScript(agentName: string, agentId: string): string {
|
||||||
return `#!/usr/bin/env node
|
return `#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -316,9 +317,10 @@ const path = require('node:path');
|
|||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
|
|
||||||
const AGENT_NAME = ${JSON.stringify(agentName)};
|
const AGENT_NAME = ${JSON.stringify(agentName)};
|
||||||
|
const AGENT_ID = ${JSON.stringify(agentId)};
|
||||||
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
||||||
const CONFIG_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.json');
|
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
|
||||||
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.status.json');
|
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'status-cache.json');
|
||||||
const CACHE_TTL_MS = 60000; // 60 seconds
|
const CACHE_TTL_MS = 60000; // 60 seconds
|
||||||
|
|
||||||
// Discard stdin immediately so the process can exit cleanly
|
// Discard stdin immediately so the process can exit cleanly
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
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 = 'kirocli';
|
||||||
|
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
||||||
|
|
||||||
|
function resolveConfigDir(): string {
|
||||||
|
return entry.configDir.replace(/^~/, os.homedir());
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveConfigPath(): string {
|
||||||
|
return path.join(resolveConfigDir(), entry.configFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHookCommand(scriptPath: string): string {
|
||||||
|
return `node "${scriptPath}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isElydoraCommand(cmd: string, agentId?: string): boolean {
|
||||||
|
if (!cmd.includes('elydora')) return false;
|
||||||
|
if (agentId) {
|
||||||
|
return cmd.includes(agentId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterElydoraEntries(arr: Array<Record<string, unknown>>, agentId?: string): Array<Record<string, unknown>> {
|
||||||
|
return arr.filter((entry) => {
|
||||||
|
if (Array.isArray(entry.hooks)) {
|
||||||
|
const cmds = entry.hooks as Array<Record<string, unknown>>;
|
||||||
|
return !cmds.some((h) => typeof h.command === 'string' && isElydoraCommand(h.command, agentId));
|
||||||
|
}
|
||||||
|
if (typeof entry.command === 'string') {
|
||||||
|
return !isElydoraCommand(entry.command, agentId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const kirocliPlugin: AgentPlugin = {
|
||||||
|
async install(config: InstallConfig): Promise<void> {
|
||||||
|
const configDir = resolveConfigDir();
|
||||||
|
await fsp.mkdir(configDir, { recursive: true });
|
||||||
|
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
let settings: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
|
settings = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist or isn't valid JSON — start fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
||||||
|
settings.hooks = {};
|
||||||
|
}
|
||||||
|
const hooks = settings.hooks as Record<string, unknown>;
|
||||||
|
|
||||||
|
// --- PreToolUse (guard — freeze enforcement) ---
|
||||||
|
if (!Array.isArray(hooks.PreToolUse)) {
|
||||||
|
hooks.PreToolUse = [];
|
||||||
|
}
|
||||||
|
const preFiltered = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>);
|
||||||
|
preFiltered.push({
|
||||||
|
matcher: '*',
|
||||||
|
hooks: [{ type: 'command', command: buildHookCommand(config.guardScriptPath), timeout_ms: 5000 }],
|
||||||
|
});
|
||||||
|
hooks.PreToolUse = preFiltered;
|
||||||
|
|
||||||
|
// --- PostToolUse (audit logging) ---
|
||||||
|
if (!Array.isArray(hooks.PostToolUse)) {
|
||||||
|
hooks.PostToolUse = [];
|
||||||
|
}
|
||||||
|
const postFiltered = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>);
|
||||||
|
postFiltered.push({
|
||||||
|
matcher: '*',
|
||||||
|
hooks: [{ type: 'command', command: buildHookCommand(config.hookScriptPath), timeout_ms: 5000 }],
|
||||||
|
});
|
||||||
|
hooks.PostToolUse = postFiltered;
|
||||||
|
|
||||||
|
settings.hooks = hooks;
|
||||||
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
|
},
|
||||||
|
|
||||||
|
async uninstall(agentId?: string): Promise<void> {
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
|
let settings: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
|
settings = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return; // Nothing to uninstall
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks = settings.hooks as Record<string, unknown> | undefined;
|
||||||
|
if (!hooks) return;
|
||||||
|
|
||||||
|
if (Array.isArray(hooks.PreToolUse)) {
|
||||||
|
hooks.PreToolUse = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
|
}
|
||||||
|
if (Array.isArray(hooks.PostToolUse)) {
|
||||||
|
hooks.PostToolUse = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
|
},
|
||||||
|
|
||||||
|
async status(): Promise<PluginStatus> {
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
|
let hookConfigured = false;
|
||||||
|
let hookScriptPath = '';
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
|
const settings = JSON.parse(raw);
|
||||||
|
const hooks = settings.hooks as Record<string, unknown> | undefined;
|
||||||
|
if (hooks) {
|
||||||
|
const checkArr = (arr: unknown) => {
|
||||||
|
if (!Array.isArray(arr)) return false;
|
||||||
|
return (arr as Array<Record<string, unknown>>).some((entry) => {
|
||||||
|
if (Array.isArray(entry.hooks)) {
|
||||||
|
return (entry.hooks as Array<Record<string, unknown>>).some(
|
||||||
|
(h) => typeof h.command === 'string' && h.command.includes('elydora'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return typeof entry.command === 'string' && entry.command.includes('elydora');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
hookConfigured = checkArr(hooks.PreToolUse) && checkArr(hooks.PostToolUse);
|
||||||
|
|
||||||
|
// Extract hook script path from PostToolUse command
|
||||||
|
if (hookConfigured && Array.isArray(hooks.PostToolUse)) {
|
||||||
|
for (const e of hooks.PostToolUse as Array<Record<string, unknown>>) {
|
||||||
|
if (Array.isArray(e.hooks)) {
|
||||||
|
for (const h of e.hooks as Array<Record<string, unknown>>) {
|
||||||
|
const cmd = h.command as string;
|
||||||
|
if (cmd && cmd.includes('elydora')) {
|
||||||
|
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Config not readable
|
||||||
|
}
|
||||||
|
|
||||||
|
let hookScriptExists = false;
|
||||||
|
if (hookScriptPath) {
|
||||||
|
try {
|
||||||
|
await fsp.access(hookScriptPath);
|
||||||
|
hookScriptExists = true;
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: hookConfigured && hookScriptExists,
|
||||||
|
agentName: AGENT_KEY,
|
||||||
|
displayName: entry.name,
|
||||||
|
hookConfigured,
|
||||||
|
hookScriptExists,
|
||||||
|
configPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
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 = 'kiroide';
|
||||||
|
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
||||||
|
|
||||||
|
function resolveConfigDir(): string {
|
||||||
|
// .kiro/hooks is project-relative, but for global install use home dir
|
||||||
|
return path.join(os.homedir(), entry.configDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveConfigPath(): string {
|
||||||
|
return path.join(resolveConfigDir(), entry.configFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const kiroidePlugin: AgentPlugin = {
|
||||||
|
async install(config: InstallConfig): Promise<void> {
|
||||||
|
const configDir = resolveConfigDir();
|
||||||
|
await fsp.mkdir(configDir, { recursive: true });
|
||||||
|
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
|
const hookConfig = {
|
||||||
|
name: 'Elydora Audit',
|
||||||
|
description: 'Sends tool-use events to the Elydora tamper-evident audit platform',
|
||||||
|
version: '1.0.0',
|
||||||
|
hooks: {
|
||||||
|
pre_tool_use: {
|
||||||
|
command: `node "${config.guardScriptPath}"`,
|
||||||
|
timeout_ms: 5000,
|
||||||
|
},
|
||||||
|
post_tool_use: {
|
||||||
|
command: `node "${config.hookScriptPath}"`,
|
||||||
|
timeout_ms: 5000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await fsp.writeFile(configPath, JSON.stringify(hookConfig, null, 2) + '\n', '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 = '';
|
||||||
|
try {
|
||||||
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
|
const config = JSON.parse(raw);
|
||||||
|
hookConfigured = !!(config.hooks && config.hooks.pre_tool_use && config.hooks.post_tool_use);
|
||||||
|
|
||||||
|
// Extract hook script path from post_tool_use command
|
||||||
|
if (hookConfigured && config.hooks.post_tool_use) {
|
||||||
|
const cmd = config.hooks.post_tool_use.command as string;
|
||||||
|
if (cmd && cmd.includes('elydora')) {
|
||||||
|
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Config not readable
|
||||||
|
}
|
||||||
|
|
||||||
|
let hookScriptExists = false;
|
||||||
|
if (hookScriptPath) {
|
||||||
|
try {
|
||||||
|
await fsp.access(hookScriptPath);
|
||||||
|
hookScriptExists = true;
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: hookConfigured && hookScriptExists,
|
||||||
|
agentName: AGENT_KEY,
|
||||||
|
displayName: entry.name,
|
||||||
|
hookConfigured,
|
||||||
|
hookScriptExists,
|
||||||
|
configPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|||||||
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
|
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
|
||||||
import { SUPPORTED_AGENTS } from './registry.js';
|
import { SUPPORTED_AGENTS } from './registry.js';
|
||||||
|
|
||||||
const AGENT_KEY = 'augment';
|
const AGENT_KEY = 'letta';
|
||||||
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
||||||
|
|
||||||
function resolveConfigDir(): string {
|
function resolveConfigDir(): string {
|
||||||
@@ -19,18 +19,28 @@ function buildHookCommand(scriptPath: string): string {
|
|||||||
return `node "${scriptPath}"`;
|
return `node "${scriptPath}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterElydoraEntries(arr: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
function isElydoraCommand(cmd: string, agentId?: string): boolean {
|
||||||
|
if (!cmd.includes('elydora')) return false;
|
||||||
|
if (agentId) {
|
||||||
|
return cmd.includes(agentId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterElydoraEntries(arr: Array<Record<string, unknown>>, agentId?: string): Array<Record<string, unknown>> {
|
||||||
return arr.filter((entry) => {
|
return arr.filter((entry) => {
|
||||||
if (Array.isArray(entry.hooks)) {
|
if (Array.isArray(entry.hooks)) {
|
||||||
const cmds = entry.hooks as Array<Record<string, unknown>>;
|
const cmds = entry.hooks as Array<Record<string, unknown>>;
|
||||||
return !cmds.some((h) => typeof h.command === 'string' && h.command.includes('elydora'));
|
return !cmds.some((h) => typeof h.command === 'string' && isElydoraCommand(h.command, agentId));
|
||||||
|
}
|
||||||
|
if (typeof entry.command === 'string') {
|
||||||
|
return !isElydoraCommand(entry.command, agentId);
|
||||||
}
|
}
|
||||||
if (typeof entry.command === 'string') return !entry.command.includes('elydora');
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const augmentPlugin: AgentPlugin = {
|
export const lettaPlugin: AgentPlugin = {
|
||||||
async install(config: InstallConfig): Promise<void> {
|
async install(config: InstallConfig): Promise<void> {
|
||||||
const configDir = resolveConfigDir();
|
const configDir = resolveConfigDir();
|
||||||
await fsp.mkdir(configDir, { recursive: true });
|
await fsp.mkdir(configDir, { recursive: true });
|
||||||
@@ -42,7 +52,7 @@ export const augmentPlugin: AgentPlugin = {
|
|||||||
const raw = await fsp.readFile(configPath, 'utf-8');
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
settings = JSON.parse(raw);
|
settings = JSON.parse(raw);
|
||||||
} catch {
|
} catch {
|
||||||
// Start fresh
|
// File doesn't exist or isn't valid JSON — start fresh
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
||||||
@@ -56,6 +66,7 @@ export const augmentPlugin: AgentPlugin = {
|
|||||||
}
|
}
|
||||||
const preFiltered = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>);
|
const preFiltered = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>);
|
||||||
preFiltered.push({
|
preFiltered.push({
|
||||||
|
matcher: '*',
|
||||||
hooks: [{ type: 'command', command: buildHookCommand(config.guardScriptPath) }],
|
hooks: [{ type: 'command', command: buildHookCommand(config.guardScriptPath) }],
|
||||||
});
|
});
|
||||||
hooks.PreToolUse = preFiltered;
|
hooks.PreToolUse = preFiltered;
|
||||||
@@ -66,6 +77,7 @@ export const augmentPlugin: AgentPlugin = {
|
|||||||
}
|
}
|
||||||
const postFiltered = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>);
|
const postFiltered = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>);
|
||||||
postFiltered.push({
|
postFiltered.push({
|
||||||
|
matcher: '*',
|
||||||
hooks: [{ type: 'command', command: buildHookCommand(config.hookScriptPath) }],
|
hooks: [{ type: 'command', command: buildHookCommand(config.hookScriptPath) }],
|
||||||
});
|
});
|
||||||
hooks.PostToolUse = postFiltered;
|
hooks.PostToolUse = postFiltered;
|
||||||
@@ -74,7 +86,7 @@ export const augmentPlugin: AgentPlugin = {
|
|||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
},
|
},
|
||||||
|
|
||||||
async uninstall(): Promise<void> {
|
async uninstall(agentId?: string): Promise<void> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
let settings: Record<string, unknown>;
|
let settings: Record<string, unknown>;
|
||||||
@@ -82,17 +94,17 @@ export const augmentPlugin: AgentPlugin = {
|
|||||||
const raw = await fsp.readFile(configPath, 'utf-8');
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
settings = JSON.parse(raw);
|
settings = JSON.parse(raw);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return; // Nothing to uninstall
|
||||||
}
|
}
|
||||||
|
|
||||||
const hooks = settings.hooks as Record<string, unknown> | undefined;
|
const hooks = settings.hooks as Record<string, unknown> | undefined;
|
||||||
if (!hooks) return;
|
if (!hooks) return;
|
||||||
|
|
||||||
if (Array.isArray(hooks.PreToolUse)) {
|
if (Array.isArray(hooks.PreToolUse)) {
|
||||||
hooks.PreToolUse = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>);
|
hooks.PreToolUse = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
if (Array.isArray(hooks.PostToolUse)) {
|
if (Array.isArray(hooks.PostToolUse)) {
|
||||||
hooks.PostToolUse = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>);
|
hooks.PostToolUse = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>, agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||||
@@ -100,9 +112,9 @@ export const augmentPlugin: AgentPlugin = {
|
|||||||
|
|
||||||
async status(): Promise<PluginStatus> {
|
async status(): Promise<PluginStatus> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
const hookScriptPath = path.join(os.homedir(), '.elydora', 'hooks', `${AGENT_KEY}-hook.js`);
|
|
||||||
|
|
||||||
let hookConfigured = false;
|
let hookConfigured = false;
|
||||||
|
let hookScriptPath = '';
|
||||||
try {
|
try {
|
||||||
const raw = await fsp.readFile(configPath, 'utf-8');
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
const settings = JSON.parse(raw);
|
const settings = JSON.parse(raw);
|
||||||
@@ -120,17 +132,33 @@ export const augmentPlugin: AgentPlugin = {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
hookConfigured = checkArr(hooks.PreToolUse) && checkArr(hooks.PostToolUse);
|
hookConfigured = checkArr(hooks.PreToolUse) && checkArr(hooks.PostToolUse);
|
||||||
|
|
||||||
|
// Extract hook script path from PostToolUse command
|
||||||
|
if (hookConfigured && Array.isArray(hooks.PostToolUse)) {
|
||||||
|
for (const e of hooks.PostToolUse as Array<Record<string, unknown>>) {
|
||||||
|
if (Array.isArray(e.hooks)) {
|
||||||
|
for (const h of e.hooks as Array<Record<string, unknown>>) {
|
||||||
|
const cmd = h.command as string;
|
||||||
|
if (cmd && cmd.includes('elydora')) {
|
||||||
|
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Config not readable
|
// Config not readable
|
||||||
}
|
}
|
||||||
|
|
||||||
let hookScriptExists = false;
|
let hookScriptExists = false;
|
||||||
try {
|
if (hookScriptPath) {
|
||||||
await fsp.access(hookScriptPath);
|
try {
|
||||||
hookScriptExists = true;
|
await fsp.access(hookScriptPath);
|
||||||
} catch {
|
hookScriptExists = true;
|
||||||
// File doesn't exist
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
+39
-27
@@ -8,7 +8,6 @@ const AGENT_KEY = 'opencode';
|
|||||||
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
||||||
|
|
||||||
function resolveConfigDir(): string {
|
function resolveConfigDir(): string {
|
||||||
// .opencode/plugins is project-relative, but for global install use home dir
|
|
||||||
return path.join(os.homedir(), entry.configDir);
|
return path.join(os.homedir(), entry.configDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,50 +19,54 @@ function generatePluginScript(hookScriptPath: string, guardScriptPath: string):
|
|||||||
return `// Elydora Audit Plugin for OpenCode
|
return `// Elydora Audit Plugin for OpenCode
|
||||||
// Generated by @elydora/sdk — DO NOT EDIT
|
// Generated by @elydora/sdk — DO NOT EDIT
|
||||||
|
|
||||||
const { spawn, spawnSync } = require('node:child_process');
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
module.exports = {
|
export const ElydoraAuditPlugin = async (ctx) => {
|
||||||
name: 'elydora-audit',
|
return {
|
||||||
version: '1.0.0',
|
"tool.execute.before": async (input, output) => {
|
||||||
description: 'Sends tool-use events to the Elydora tamper-evident audit platform',
|
// Guard — blocks tool if agent is frozen
|
||||||
|
|
||||||
hooks: {
|
|
||||||
preToolUse(context) {
|
|
||||||
// Synchronous guard — blocks tool if agent is frozen
|
|
||||||
try {
|
try {
|
||||||
const result = spawnSync('node', ['__ELYDORA_GUARD_SCRIPT_PATH__'], {
|
const result = spawnSync('node', ['__ELYDORA_GUARD_SCRIPT_PATH__'], {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
stdio: ['pipe', 'ignore', 'pipe'],
|
stdio: ['pipe', 'ignore', 'pipe'],
|
||||||
});
|
});
|
||||||
if (result.status !== 0) {
|
if (result.status === 2) {
|
||||||
const msg = result.stderr ? result.stderr.toString().trim() : 'Agent is frozen by Elydora.';
|
const msg = result.stderr ? result.stderr.toString().trim() : 'Agent is frozen by Elydora.';
|
||||||
return { blocked: true, reason: msg };
|
// Signal denial by setting a blocked flag
|
||||||
|
if (output && typeof output === 'object') {
|
||||||
|
output.blocked = true;
|
||||||
|
output.reason = msg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fail-open — allow if guard can't run
|
// Fail-open — allow if guard can't run
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
postToolUse(context) {
|
"tool.execute.after": async (input, output) => {
|
||||||
try {
|
try {
|
||||||
const input = JSON.stringify({
|
const toolName = input?.tool || 'unknown';
|
||||||
tool_name: context.toolName || context.tool_name || 'unknown',
|
const toolInput = output?.args || input?.args || {};
|
||||||
tool_input: context.toolInput || context.tool_input || {},
|
const sessionId = ctx?.project?.name || 'unknown';
|
||||||
session_id: context.sessionId || context.session_id || 'unknown',
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
tool_name: toolName,
|
||||||
|
tool_input: toolInput,
|
||||||
|
session_id: sessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const child = spawn('node', ['__ELYDORA_HOOK_SCRIPT_PATH__'], {
|
const child = spawn('node', ['__ELYDORA_HOOK_SCRIPT_PATH__'], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: ['pipe', 'ignore', 'ignore'],
|
stdio: ['pipe', 'ignore', 'ignore'],
|
||||||
});
|
});
|
||||||
child.stdin.write(input);
|
child.stdin.write(data);
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
child.unref();
|
child.unref();
|
||||||
} catch {
|
} catch {
|
||||||
// Fire-and-forget — never block the developer
|
// Fire-and-forget — never block the developer
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,7 @@ export const opencodePlugin: AgentPlugin = {
|
|||||||
await fsp.writeFile(configPath, script, 'utf-8');
|
await fsp.writeFile(configPath, script, 'utf-8');
|
||||||
},
|
},
|
||||||
|
|
||||||
async uninstall(): Promise<void> {
|
async uninstall(_agentId?: string): Promise<void> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
try {
|
try {
|
||||||
await fsp.unlink(configPath);
|
await fsp.unlink(configPath);
|
||||||
@@ -92,22 +95,31 @@ export const opencodePlugin: AgentPlugin = {
|
|||||||
|
|
||||||
async status(): Promise<PluginStatus> {
|
async status(): Promise<PluginStatus> {
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
const hookScriptPath = path.join(os.homedir(), '.elydora', 'hooks', `${AGENT_KEY}-hook.js`);
|
|
||||||
|
|
||||||
let hookConfigured = false;
|
let hookConfigured = false;
|
||||||
|
let hookScriptPath = '';
|
||||||
try {
|
try {
|
||||||
await fsp.access(configPath);
|
const raw = await fsp.readFile(configPath, 'utf-8');
|
||||||
hookConfigured = true;
|
hookConfigured = true;
|
||||||
|
|
||||||
|
// Extract hook script path from plugin script content
|
||||||
|
// The plugin script contains a spawn call like: spawn('node', ['<path>'])
|
||||||
|
const match = raw.match(/spawn\('node',\s*\['([^']*elydora[^']*)'\]/);
|
||||||
|
if (match) {
|
||||||
|
hookScriptPath = match[1];
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// File doesn't exist
|
// File doesn't exist
|
||||||
}
|
}
|
||||||
|
|
||||||
let hookScriptExists = false;
|
let hookScriptExists = false;
|
||||||
try {
|
if (hookScriptPath) {
|
||||||
await fsp.access(hookScriptPath);
|
try {
|
||||||
hookScriptExists = true;
|
await fsp.access(hookScriptPath);
|
||||||
} catch {
|
hookScriptExists = true;
|
||||||
// File doesn't exist
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export const SUPPORTED_AGENTS: ReadonlyMap<string, AgentRegistryEntry> = new Map
|
|||||||
['claudecode', { name: 'Claude Code', configDir: '~/.claude', configFile: 'settings.json' }],
|
['claudecode', { name: 'Claude Code', configDir: '~/.claude', configFile: 'settings.json' }],
|
||||||
['cursor', { name: 'Cursor', configDir: '.cursor', configFile: 'hooks.json' }],
|
['cursor', { name: 'Cursor', configDir: '.cursor', configFile: 'hooks.json' }],
|
||||||
['gemini', { name: 'Gemini CLI', configDir: '~/.gemini', configFile: 'settings.json' }],
|
['gemini', { name: 'Gemini CLI', configDir: '~/.gemini', configFile: 'settings.json' }],
|
||||||
['augment', { name: 'Augment Code', configDir: '~/.augment', configFile: 'settings.json' }],
|
['kirocli', { name: 'Kiro CLI', configDir: '~/.kiro', configFile: 'settings.json' }],
|
||||||
['kiro', { name: 'Kiro', configDir: '.kiro/hooks', configFile: 'elydora-audit.kiro.hook' }],
|
['kiroide', { name: 'Kiro IDE', configDir: '.kiro/hooks', configFile: 'elydora-audit.kiro.hook' }],
|
||||||
['opencode', { name: 'OpenCode', configDir: '.opencode/plugins', configFile: 'elydora-audit.js' }],
|
['opencode', { name: 'OpenCode', configDir: '.config/opencode/plugins', configFile: 'elydora-audit.mjs' }],
|
||||||
|
['copilot', { name: 'Copilot CLI', configDir: '.github/hooks', configFile: 'hooks.json' }],
|
||||||
|
['letta', { name: 'Letta Code', configDir: '~/.letta', configFile: 'settings.json' }],
|
||||||
]);
|
]);
|
||||||
Reference in new issue
Block a user