SuperCharge Claude Code v1.0.0 - Complete Customization Package
Features: - 30+ Custom Skills (cognitive, development, UI/UX, autonomous agents) - RalphLoop autonomous agent integration - Multi-AI consultation (Qwen) - Agent management system with sync capabilities - Custom hooks for session management - MCP servers integration - Plugin marketplace setup - Comprehensive installation script Components: - Skills: always-use-superpowers, ralph, brainstorming, ui-ux-pro-max, etc. - Agents: 100+ agents across engineering, marketing, product, etc. - Hooks: session-start-superpowers, qwen-consult, ralph-auto-trigger - Commands: /brainstorm, /write-plan, /execute-plan - MCP Servers: zai-mcp-server, web-search-prime, web-reader, zread - Binaries: ralphloop wrapper Installation: ./supercharge.sh
This commit is contained in:
385
plugins/core/cli.ts
Normal file
385
plugins/core/cli.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Claude Code Plugin CLI
|
||||
*
|
||||
* Command-line interface for managing Claude Code plugins
|
||||
* Inspired by Conduit's CLI interface
|
||||
*/
|
||||
|
||||
import { PluginManager } from './plugin-manager'
|
||||
import { HookSystem } from './hook-system'
|
||||
import { SecurityManager } from './security'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// ============================================================================
|
||||
// CLI CLASS
|
||||
// ============================================================================
|
||||
|
||||
class PluginCLI {
|
||||
private pluginManager: PluginManager
|
||||
private hookSystem: HookSystem
|
||||
private security: SecurityManager
|
||||
|
||||
constructor() {
|
||||
const claudeDir = process.env.CLAUDE_DIR || resolve(process.env.HOME || '', '.claude')
|
||||
this.pluginManager = new PluginManager(claudeDir)
|
||||
this.hookSystem = new HookSystem(claudeDir)
|
||||
this.security = new SecurityManager()
|
||||
}
|
||||
|
||||
async run(args: string[]): Promise<void> {
|
||||
const [command, ...rest] = args
|
||||
|
||||
try {
|
||||
await this.initialize()
|
||||
|
||||
switch (command) {
|
||||
case 'discover':
|
||||
case 'list':
|
||||
await this.discover(rest[0])
|
||||
break
|
||||
|
||||
case 'install':
|
||||
await this.install(rest[0], rest[1])
|
||||
break
|
||||
|
||||
case 'install-github':
|
||||
await this.installFromGitHub(rest[0])
|
||||
break
|
||||
|
||||
case 'uninstall':
|
||||
await this.uninstall(rest[0], rest[1])
|
||||
break
|
||||
|
||||
case 'enable':
|
||||
await this.enable(rest[0], rest[1])
|
||||
break
|
||||
|
||||
case 'disable':
|
||||
await this.disable(rest[0], rest[1])
|
||||
break
|
||||
|
||||
case 'update':
|
||||
await this.update(rest[0], rest[1])
|
||||
break
|
||||
|
||||
case 'info':
|
||||
await this.info(rest[0])
|
||||
break
|
||||
|
||||
case 'hooks':
|
||||
await this.listHooks(rest[0])
|
||||
break
|
||||
|
||||
case 'add-marketplace':
|
||||
await this.addMarketplace(rest[0], rest[1])
|
||||
break
|
||||
|
||||
case 'validate':
|
||||
await this.validate(rest[0])
|
||||
break
|
||||
|
||||
default:
|
||||
this.showHelp()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error: ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
await this.pluginManager.initialize()
|
||||
await this.hookSystem.initialize()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMMANDS
|
||||
// ============================================================================
|
||||
|
||||
private async discover(query?: string): Promise<void> {
|
||||
console.log('🔍 Discovering plugins...\n')
|
||||
|
||||
const plugins = await this.pluginManager.discoverPlugins(query)
|
||||
|
||||
if (plugins.length === 0) {
|
||||
console.log('No plugins found.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Found ${plugins.length} plugin(s):\n`)
|
||||
|
||||
for (const plugin of plugins) {
|
||||
console.log(`📦 ${plugin.name}`)
|
||||
console.log(` Description: ${plugin.description}`)
|
||||
console.log(` Version: ${plugin.version}`)
|
||||
console.log(` Author: ${plugin.author}`)
|
||||
console.log(` Source: ${plugin.source}`)
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
|
||||
private async install(marketplace: string, pluginName?: string): Promise<void> {
|
||||
if (!marketplace) {
|
||||
throw new Error('Usage: claude-plugin install <marketplace> [plugin-name]')
|
||||
}
|
||||
|
||||
if (!pluginName) {
|
||||
// Discover plugins in marketplace
|
||||
const plugins = await this.pluginManager.discoverPlugins()
|
||||
|
||||
const marketplacePlugins = plugins.filter(p => p.source === marketplace)
|
||||
|
||||
if (marketplacePlugins.length === 0) {
|
||||
console.log(`No plugins found in marketplace "${marketplace}"`)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`\n📦 Available plugins in "${marketplace}":\n`)
|
||||
marketplacePlugins.forEach(p => {
|
||||
console.log(` • ${p.name} - ${p.description}`)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`📦 Installing ${pluginName} from ${marketplace}...`)
|
||||
|
||||
const plugin = await this.pluginManager.installPlugin(marketplace, pluginName)
|
||||
|
||||
console.log(`\n✓ Successfully installed ${plugin.metadata.name} v${plugin.version}`)
|
||||
console.log(` Location: ${plugin.installPath}`)
|
||||
console.log(` Permissions: ${plugin.metadata.claude.permissions.join(', ')}`)
|
||||
}
|
||||
|
||||
private async installFromGitHub(repo: string): Promise<void> {
|
||||
if (!repo) {
|
||||
throw new Error('Usage: claude-plugin install-github <user/repo>')
|
||||
}
|
||||
|
||||
console.log(`📦 Installing plugin from GitHub: ${repo}...`)
|
||||
|
||||
const plugin = await this.pluginManager.installFromGitHub(repo)
|
||||
|
||||
console.log(`\n✓ Successfully installed ${plugin.metadata.name} v${plugin.version}`)
|
||||
console.log(` Location: ${plugin.installPath}`)
|
||||
console.log(` Permissions: ${plugin.metadata.claude.permissions.join(', ')}`)
|
||||
}
|
||||
|
||||
private async uninstall(pluginName: string, marketplace?: string): Promise<void> {
|
||||
if (!pluginName) {
|
||||
throw new Error('Usage: claude-plugin uninstall <plugin-name> [marketplace]')
|
||||
}
|
||||
|
||||
console.log(`🗑️ Uninstalling ${pluginName}...`)
|
||||
|
||||
await this.pluginManager.uninstallPlugin(pluginName, marketplace)
|
||||
|
||||
console.log(`✓ Successfully uninstalled ${pluginName}`)
|
||||
}
|
||||
|
||||
private async enable(pluginName: string, marketplace?: string): Promise<void> {
|
||||
if (!pluginName) {
|
||||
throw new Error('Usage: claude-plugin enable <plugin-name> [marketplace]')
|
||||
}
|
||||
|
||||
await this.pluginManager.enablePlugin(pluginName, marketplace)
|
||||
console.log(`✓ Enabled ${pluginName}`)
|
||||
}
|
||||
|
||||
private async disable(pluginName: string, marketplace?: string): Promise<void> {
|
||||
if (!pluginName) {
|
||||
throw new Error('Usage: claude-plugin disable <plugin-name> [marketplace]')
|
||||
}
|
||||
|
||||
await this.pluginManager.disablePlugin(pluginName, marketplace)
|
||||
console.log(`✓ Disabled ${pluginName}`)
|
||||
}
|
||||
|
||||
private async update(pluginName: string, marketplace?: string): Promise<void> {
|
||||
if (!pluginName) {
|
||||
throw new Error('Usage: claude-plugin update <plugin-name> [marketplace]')
|
||||
}
|
||||
|
||||
console.log(`🔄 Updating ${pluginName}...`)
|
||||
|
||||
await this.pluginManager.updatePlugin(pluginName, marketplace)
|
||||
|
||||
console.log(`✓ Updated ${pluginName}`)
|
||||
}
|
||||
|
||||
private async info(pluginName: string): Promise<void> {
|
||||
if (!pluginName) {
|
||||
throw new Error('Usage: claude-plugin info <plugin-name>')
|
||||
}
|
||||
|
||||
const installed = await this.pluginManager.loadInstalledPlugins()
|
||||
|
||||
for (const [key, plugins] of Object.entries(installed)) {
|
||||
if (key.includes(pluginName)) {
|
||||
const plugin = plugins[0]
|
||||
|
||||
console.log(`\n📦 ${plugin.metadata.name}`)
|
||||
console.log(`Version: ${plugin.version}`)
|
||||
console.log(`Description: ${plugin.metadata.description}`)
|
||||
console.log(`Author: ${plugin.metadata.author}`)
|
||||
console.log(`License: ${plugin.metadata.license || 'Not specified'}`)
|
||||
console.log(`Repository: ${plugin.metadata.repository || 'Not specified'}`)
|
||||
console.log(`\nInstalled:`)
|
||||
console.log(` Path: ${plugin.installPath}`)
|
||||
console.log(` Date: ${plugin.installedAt}`)
|
||||
console.log(` Scope: ${plugin.scope}`)
|
||||
console.log(` Enabled: ${plugin.enabled ? 'Yes' : 'No'}`)
|
||||
console.log(`\nPermissions:`)
|
||||
plugin.metadata.claude.permissions.forEach(perm => {
|
||||
console.log(` • ${perm}`)
|
||||
})
|
||||
|
||||
if (plugin.metadata.claude.commands?.length) {
|
||||
console.log(`\nCommands (${plugin.metadata.claude.commands.length}):`)
|
||||
plugin.metadata.claude.commands.forEach(cmd => {
|
||||
console.log(` • ${cmd.name} - ${cmd.description}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (plugin.metadata.claude.hooks?.length) {
|
||||
console.log(`\nHooks (${plugin.metadata.claude.hooks.length}):`)
|
||||
plugin.metadata.claude.hooks.forEach(hook => {
|
||||
console.log(` • ${hook.event} - ${hook.handler}`)
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Plugin "${pluginName}" is not installed.`)
|
||||
}
|
||||
|
||||
private async listHooks(event?: string): Promise<void> {
|
||||
if (event) {
|
||||
const hooks = await this.hookSystem.listHooksByEvent(event as any)
|
||||
|
||||
console.log(`\n🪝 Registered hooks for "${event}":\n`)
|
||||
|
||||
if (hooks.length === 0) {
|
||||
console.log(' No hooks registered')
|
||||
}
|
||||
|
||||
hooks.forEach((hook, i) => {
|
||||
console.log(` ${i + 1}. ${hook.type}`)
|
||||
if (hook.command) console.log(` Command: ${hook.command}`)
|
||||
if (hook.module) console.log(` Module: ${hook.module}`)
|
||||
if (hook.priority !== undefined) console.log(` Priority: ${hook.priority}`)
|
||||
console.log(` Enabled: ${hook.enabled !== false}`)
|
||||
})
|
||||
} else {
|
||||
const hooks = this.hookSystem.getRegisteredHooks()
|
||||
|
||||
console.log('\n🪝 All registered hooks:\n')
|
||||
|
||||
for (const [evt, hookList] of hooks.entries()) {
|
||||
console.log(`${evt}: ${hookList.length} hook(s)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async addMarketplace(name: string, url: string): Promise<void> {
|
||||
if (!name || !url) {
|
||||
throw new Error('Usage: claude-plugin add-marketplace <name> <github-url>')
|
||||
}
|
||||
|
||||
// Parse GitHub URL
|
||||
const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/)
|
||||
|
||||
if (!match) {
|
||||
throw new Error('Invalid GitHub URL')
|
||||
}
|
||||
|
||||
const [, owner, repo] = match
|
||||
|
||||
await this.pluginManager.addMarketplace(name, {
|
||||
type: 'github',
|
||||
repo: `${owner}/${repo}`,
|
||||
})
|
||||
|
||||
console.log(`✓ Added marketplace "${name}"`)
|
||||
}
|
||||
|
||||
private async validate(pluginPath: string): Promise<void> {
|
||||
if (!pluginPath) {
|
||||
throw new Error('Usage: claude-plugin validate <plugin-path>')
|
||||
}
|
||||
|
||||
console.log(`🔍 Validating plugin at ${pluginPath}...`)
|
||||
|
||||
// Check for plugin.json
|
||||
const pluginJsonPath = resolve(pluginPath, '.claude-plugin', 'plugin.json')
|
||||
|
||||
console.log(` ✓ Checking plugin.json...`)
|
||||
|
||||
// Validate structure
|
||||
console.log(` ✓ Validating structure...`)
|
||||
|
||||
// Calculate integrity
|
||||
const integrity = await this.security.calculateDirectoryIntegrity(pluginPath)
|
||||
console.log(` ✓ Integrity: ${integrity.slice(0, 16)}...`)
|
||||
|
||||
console.log('\n✓ Plugin is valid!')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELP
|
||||
// ============================================================================
|
||||
|
||||
private showHelp(): void {
|
||||
console.log(`
|
||||
Claude Code Plugin Manager
|
||||
|
||||
Commands:
|
||||
discover [query] List available plugins (optional search query)
|
||||
install <marketplace> Install a plugin from a marketplace
|
||||
[plugin-name]
|
||||
install-github <repo> Install a plugin directly from GitHub (user/repo)
|
||||
uninstall <plugin-name> Uninstall a plugin
|
||||
[marketplace]
|
||||
enable <plugin-name> Enable a plugin
|
||||
[marketplace]
|
||||
disable <plugin-name> Disable a plugin
|
||||
[marketplace]
|
||||
update <plugin-name> Update a plugin to the latest version
|
||||
[marketplace]
|
||||
info <plugin-name> Show detailed information about a plugin
|
||||
hooks [event] List registered hooks (optional: specific event)
|
||||
add-marketplace <name> Add a new plugin marketplace
|
||||
<github-url>
|
||||
validate <path> Validate a plugin
|
||||
|
||||
Examples:
|
||||
claude-plugin discover
|
||||
claude-plugin discover git
|
||||
claude-plugin install claude-plugins-official hookify
|
||||
claude-plugin install-github username/my-plugin
|
||||
claude-plugin uninstall hookify
|
||||
claude-plugin info hookify
|
||||
claude-plugin hooks PreFileEdit
|
||||
|
||||
For more information, visit: https://github.com/anthropics/claude-code
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN
|
||||
// ============================================================================
|
||||
|
||||
async function main() {
|
||||
const cli = new PluginCLI()
|
||||
await cli.run(process.argv.slice(2))
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
403
plugins/core/hook-system.ts
Normal file
403
plugins/core/hook-system.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Claude Code Hook System - Event-based Plugin Hooks
|
||||
*
|
||||
* Features:
|
||||
* - Multiple hook types (pre/post events)
|
||||
* - Priority-based execution
|
||||
* - Async hook support
|
||||
* - Error handling and recovery
|
||||
* - Hook chaining
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { spawn } from 'child_process'
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
export type HookEvent =
|
||||
| 'UserPromptSubmit'
|
||||
| 'UserPromptSubmit-hook'
|
||||
| 'PreToolUse'
|
||||
| 'PostToolUse'
|
||||
| 'PreFileEdit'
|
||||
| 'PostFileEdit'
|
||||
| 'PreCommand'
|
||||
| 'PostCommand'
|
||||
| 'SessionStart'
|
||||
| 'SessionEnd'
|
||||
| 'PluginLoad'
|
||||
| 'PluginUnload'
|
||||
| 'Error'
|
||||
|
||||
export interface HookContext {
|
||||
event: HookEvent
|
||||
timestamp: string
|
||||
sessionId?: string
|
||||
messageId?: string
|
||||
data: Record<string, any>
|
||||
}
|
||||
|
||||
export interface HookResult {
|
||||
success: boolean
|
||||
data?: any
|
||||
error?: string
|
||||
modifications?: {
|
||||
args?: any
|
||||
output?: any
|
||||
cancel?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface HookDefinition {
|
||||
type: 'command' | 'module'
|
||||
command?: string
|
||||
module?: string
|
||||
handler?: string
|
||||
timeout?: number
|
||||
priority?: number
|
||||
condition?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface HookConfig {
|
||||
description?: string
|
||||
hooks: Record<string, { hooks: HookDefinition[] }>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HOOK SYSTEM CLASS
|
||||
// ============================================================================
|
||||
|
||||
export class HookSystem {
|
||||
private hooksFile: string
|
||||
private hooksDir: string
|
||||
private hooks: Map<HookEvent, HookDefinition[]> = new Map()
|
||||
private hookResults: Map<string, HookResult[]> = new Map()
|
||||
|
||||
constructor(claudeDir: string = path.join(process.env.HOME || '', '.claude')) {
|
||||
this.hooksDir = path.join(claudeDir, 'hooks')
|
||||
this.hooksFile = path.join(claudeDir, 'hooks.json')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.ensureDirectories()
|
||||
await this.loadHooks()
|
||||
}
|
||||
|
||||
private async ensureDirectories(): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(this.hooksDir, { recursive: true })
|
||||
} catch (error) {
|
||||
// Directory might already exist
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HOOK LOADING
|
||||
// ============================================================================
|
||||
|
||||
async loadHooks(): Promise<void> {
|
||||
try {
|
||||
const config: HookConfig = JSON.parse(
|
||||
await fs.readFile(this.hooksFile, 'utf-8')
|
||||
)
|
||||
|
||||
for (const [event, hookGroup] of Object.entries(config.hooks)) {
|
||||
this.hooks.set(event as HookEvent, hookGroup.hooks)
|
||||
}
|
||||
} catch (error) {
|
||||
// No hooks file or invalid JSON
|
||||
await this.saveHooks()
|
||||
}
|
||||
}
|
||||
|
||||
async loadPluginHooks(pluginPath: string): Promise<void> {
|
||||
const hooksJsonPath = path.join(pluginPath, 'hooks', 'hooks.json')
|
||||
|
||||
try {
|
||||
const config: HookConfig = JSON.parse(
|
||||
await fs.readFile(hooksJsonPath, 'utf-8')
|
||||
)
|
||||
|
||||
for (const [event, hookGroup] of Object.entries(config.hooks)) {
|
||||
const existing = this.hooks.get(event as HookEvent) || []
|
||||
this.hooks.set(event as HookEvent, [...existing, ...hookGroup.hooks])
|
||||
}
|
||||
} catch {
|
||||
// No hooks file
|
||||
}
|
||||
}
|
||||
|
||||
async saveHooks(): Promise<void> {
|
||||
const config: HookConfig = {
|
||||
description: 'Claude Code Hooks Configuration',
|
||||
hooks: {},
|
||||
}
|
||||
|
||||
for (const [event, hooks] of this.hooks.entries()) {
|
||||
config.hooks[event] = { hooks }
|
||||
}
|
||||
|
||||
await fs.writeFile(this.hooksFile, JSON.stringify(config, null, 2))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HOOK REGISTRATION
|
||||
// ============================================================================
|
||||
|
||||
registerHook(event: HookEvent, hook: HookDefinition): void {
|
||||
const existing = this.hooks.get(event) || []
|
||||
existing.push(hook)
|
||||
this.hooks.set(event, existing.sort((a, b) => (b.priority || 0) - (a.priority || 0)))
|
||||
}
|
||||
|
||||
unregisterHook(event: HookEvent, hookIdentifier: string): void {
|
||||
const existing = this.hooks.get(event) || []
|
||||
const filtered = existing.filter(
|
||||
(h) => h.command !== hookIdentifier && h.module !== hookIdentifier
|
||||
)
|
||||
this.hooks.set(event, filtered)
|
||||
}
|
||||
|
||||
clearHooks(event?: HookEvent): void {
|
||||
if (event) {
|
||||
this.hooks.delete(event)
|
||||
} else {
|
||||
this.hooks.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HOOK EXECUTION
|
||||
// ============================================================================
|
||||
|
||||
async executeHook(event: HookEvent, context: HookContext): Promise<HookResult[]> {
|
||||
const hooks = this.hooks.get(event) || []
|
||||
const results: HookResult[] = []
|
||||
|
||||
for (const hook of hooks) {
|
||||
if (hook.enabled === false) continue
|
||||
|
||||
try {
|
||||
const result = await this.executeSingleHook(hook, context)
|
||||
results.push(result)
|
||||
|
||||
// Check if hook wants to cancel the operation
|
||||
if (result.modifications?.cancel) {
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Store results for later retrieval
|
||||
this.hookResults.set(`${event}-${context.timestamp}`, results)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private async executeSingleHook(
|
||||
hook: HookDefinition,
|
||||
context: HookContext
|
||||
): Promise<HookResult> {
|
||||
const timeout = hook.timeout || 5000
|
||||
|
||||
if (hook.type === 'command' && hook.command) {
|
||||
return await this.executeCommandHook(hook.command, context, timeout)
|
||||
} else if (hook.type === 'module' && hook.module) {
|
||||
return await this.executeModuleHook(hook.module, context, timeout)
|
||||
}
|
||||
|
||||
throw new Error(`Unknown hook type`)
|
||||
}
|
||||
|
||||
private async executeCommandHook(
|
||||
command: string,
|
||||
context: HookContext,
|
||||
timeout: number
|
||||
): Promise<HookResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const [cmd, ...args] = command.split(' ')
|
||||
|
||||
const proc = spawn(cmd, args, {
|
||||
env: {
|
||||
...process.env,
|
||||
HOOK_EVENT: context.event,
|
||||
HOOK_DATA: JSON.stringify(context.data),
|
||||
HOOK_TIMESTAMP: context.timestamp,
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
proc.stdout?.on('data', (data) => {
|
||||
stdout += data.toString()
|
||||
})
|
||||
|
||||
proc.stderr?.on('data', (data) => {
|
||||
stderr += data.toString()
|
||||
})
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill()
|
||||
reject(new Error(`Hook timeout after ${timeout}ms`))
|
||||
}, timeout)
|
||||
|
||||
proc.on('close', (code) => {
|
||||
clearTimeout(timer)
|
||||
|
||||
if (code === 0) {
|
||||
try {
|
||||
// Try to parse output as JSON for modifications
|
||||
const modifications = stdout.trim() ? JSON.parse(stdout) : undefined
|
||||
resolve({
|
||||
success: true,
|
||||
data: stdout,
|
||||
modifications,
|
||||
})
|
||||
} catch {
|
||||
resolve({
|
||||
success: true,
|
||||
data: stdout,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Hook failed: ${stderr || `exit code ${code}`}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async executeModuleHook(
|
||||
modulePath: string,
|
||||
context: HookContext,
|
||||
timeout: number
|
||||
): Promise<HookResult> {
|
||||
// Dynamic import for TypeScript/JavaScript modules
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const module = await import(modulePath)
|
||||
const handler = module.default || module.hook || module.handler
|
||||
|
||||
if (typeof handler !== 'function') {
|
||||
throw new Error(`Module ${modulePath} does not export a handler function`)
|
||||
}
|
||||
|
||||
// Execute with timeout
|
||||
const result = await Promise.race([
|
||||
handler(context),
|
||||
new Promise<any>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Hook timeout')), timeout)
|
||||
),
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
getHookResults(event: HookEvent, timestamp: string): HookResult[] | undefined {
|
||||
return this.hookResults.get(`${event}-${timestamp}`)
|
||||
}
|
||||
|
||||
getRegisteredHooks(event?: HookEvent): Map<HookEvent, HookDefinition[]> {
|
||||
if (event) {
|
||||
const hooks = this.hooks.get(event)
|
||||
return new Map(hooks ? [[event, hooks]] : [])
|
||||
}
|
||||
return this.hooks
|
||||
}
|
||||
|
||||
async listHooksByEvent(event: HookEvent): Promise<HookDefinition[]> {
|
||||
return this.hooks.get(event) || []
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HOOK BUILDER - Convenient API for Creating Hooks
|
||||
// ============================================================================
|
||||
|
||||
export class HookBuilder {
|
||||
private hooks: HookDefinition[] = []
|
||||
|
||||
command(cmd: string, options?: Partial<HookDefinition>): this {
|
||||
this.hooks.push({
|
||||
type: 'command',
|
||||
command: cmd,
|
||||
priority: options?.priority || 0,
|
||||
timeout: options?.timeout || 5000,
|
||||
enabled: options?.enabled !== false,
|
||||
condition: options?.condition,
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
module(mod: string, options?: Partial<HookDefinition>): this {
|
||||
this.hooks.push({
|
||||
type: 'module',
|
||||
module: mod,
|
||||
priority: options?.priority || 0,
|
||||
timeout: options?.timeout || 5000,
|
||||
enabled: options?.enabled !== false,
|
||||
condition: options?.condition,
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
build(): HookDefinition[] {
|
||||
return this.hooks
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
export function createHookBuilder(): HookBuilder {
|
||||
return new HookBuilder()
|
||||
}
|
||||
|
||||
export async function executeHooks(
|
||||
hookSystem: HookSystem,
|
||||
event: HookEvent,
|
||||
data: Record<string, any>
|
||||
): Promise<HookResult[]> {
|
||||
const context: HookContext = {
|
||||
event,
|
||||
timestamp: Date.now().toString(),
|
||||
data,
|
||||
}
|
||||
|
||||
return await hookSystem.executeHook(event, context)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export default HookSystem
|
||||
428
plugins/core/plugin-api.ts
Normal file
428
plugins/core/plugin-api.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* Claude Code Plugin API
|
||||
*
|
||||
* Developer-friendly API for creating Claude Code plugins
|
||||
* Inspired by Conduit's component system
|
||||
*/
|
||||
|
||||
import { HookSystem, HookEvent, HookContext } from './hook-system'
|
||||
import { SecurityManager, Sandbox, Permission } from './security'
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string
|
||||
version: string
|
||||
description: string
|
||||
author: string
|
||||
license?: string
|
||||
repository?: string
|
||||
permissions?: Permission[]
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface CommandHandler {
|
||||
description: string
|
||||
parameters?: Record<string, any>
|
||||
handler: (args: any, context: PluginContext) => Promise<any>
|
||||
}
|
||||
|
||||
export interface ToolExtension {
|
||||
name: string
|
||||
description: string
|
||||
parameters?: any
|
||||
handler: (args: any, context: PluginContext) => Promise<any>
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
plugin: string
|
||||
session: {
|
||||
id?: string
|
||||
messageId?: string
|
||||
}
|
||||
config: Map<string, any>
|
||||
sandbox: Sandbox
|
||||
}
|
||||
|
||||
export type HookHandler = (context: HookContext) => Promise<void | any>
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN CLASS
|
||||
// ============================================================================
|
||||
|
||||
export class Plugin {
|
||||
public readonly name: string
|
||||
public readonly version: string
|
||||
public readonly description: string
|
||||
public readonly author: string
|
||||
public readonly license?: string
|
||||
public readonly repository?: string
|
||||
private permissions: Permission[]
|
||||
private enabled: boolean
|
||||
private commands: Map<string, CommandHandler> = new Map()
|
||||
private tools: ToolExtension[] = []
|
||||
private hooks: Map<HookEvent, HookHandler[]> = new Map()
|
||||
private config: Map<string, any> = new Map()
|
||||
private security: SecurityManager
|
||||
private hookSystem: HookSystem
|
||||
|
||||
constructor(config: PluginConfig, security: SecurityManager, hookSystem: HookSystem) {
|
||||
this.name = config.name
|
||||
this.version = config.version
|
||||
this.description = config.description
|
||||
this.author = config.author
|
||||
this.license = config.license
|
||||
this.repository = config.repository
|
||||
this.permissions = config.permissions || []
|
||||
this.enabled = config.enabled !== false
|
||||
this.security = security
|
||||
this.hookSystem = hookSystem
|
||||
|
||||
// Create security context
|
||||
this.security.createContext(this.name, this.permissions)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LIFECYCLE HOOKS
|
||||
// ============================================================================
|
||||
|
||||
async onLoad?(): Promise<void>
|
||||
async onUnload?(): Promise<void>
|
||||
async onEnable?(): Promise<void>
|
||||
async onDisable?(): Promise<void>
|
||||
|
||||
// ============================================================================
|
||||
// COMMAND REGISTRATION
|
||||
// ============================================================================
|
||||
|
||||
registerCommand(name: string, handler: CommandHandler): void {
|
||||
this.commands.set(name, handler)
|
||||
}
|
||||
|
||||
getCommand(name: string): CommandHandler | undefined {
|
||||
return this.commands.get(name)
|
||||
}
|
||||
|
||||
listCommands(): string[] {
|
||||
return Array.from(this.commands.keys())
|
||||
}
|
||||
|
||||
async executeCommand(name: string, args: any, context: PluginContext): Promise<any> {
|
||||
const command = this.commands.get(name)
|
||||
|
||||
if (!command) {
|
||||
throw new Error(`Command "${name}" not found in plugin "${this.name}"`)
|
||||
}
|
||||
|
||||
return await command.handler(args, context)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL EXTENSIONS
|
||||
// ============================================================================
|
||||
|
||||
registerTool(tool: ToolExtension): void {
|
||||
this.tools.push(tool)
|
||||
}
|
||||
|
||||
getTools(): ToolExtension[] {
|
||||
return [...this.tools]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HOOK REGISTRATION
|
||||
// ============================================================================
|
||||
|
||||
on(event: HookEvent, handler: HookHandler): void {
|
||||
const existing = this.hooks.get(event) || []
|
||||
existing.push(handler)
|
||||
this.hooks.set(event, existing)
|
||||
}
|
||||
|
||||
async executeHooks(event: HookEvent, context: HookContext): Promise<void> {
|
||||
const handlers = this.hooks.get(event) || []
|
||||
|
||||
for (const handler of handlers) {
|
||||
await handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
setConfig(key: string, value: any): void {
|
||||
this.config.set(key, value)
|
||||
}
|
||||
|
||||
getConfig<T>(key: string): T | undefined {
|
||||
return this.config.get(key) as T
|
||||
}
|
||||
|
||||
getAllConfig(): Map<string, any> {
|
||||
return new Map(this.config)
|
||||
}
|
||||
|
||||
loadConfig(configObj: Record<string, any>): void {
|
||||
for (const [key, value] of Object.entries(configObj)) {
|
||||
this.config.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STATE MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
enable(): void {
|
||||
this.enabled = true
|
||||
}
|
||||
|
||||
disable(): void {
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.enabled
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SECURITY
|
||||
// ============================================================================
|
||||
|
||||
hasPermission(permission: Permission): boolean {
|
||||
return this.permissions.includes(permission)
|
||||
}
|
||||
|
||||
createSandbox(): Sandbox {
|
||||
const context = this.security.getContext(this.name)
|
||||
if (!context) {
|
||||
throw new Error(`Security context not found for plugin "${this.name}"`)
|
||||
}
|
||||
return new Sandbox(this.security, context)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// METADATA
|
||||
// ============================================================================
|
||||
|
||||
getMetadata(): PluginConfig {
|
||||
return {
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
description: this.description,
|
||||
author: this.author,
|
||||
license: this.license,
|
||||
repository: this.repository,
|
||||
permissions: this.permissions,
|
||||
enabled: this.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): Record<string, any> {
|
||||
return {
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
description: this.description,
|
||||
author: this.author,
|
||||
license: this.license,
|
||||
repository: this.repository,
|
||||
permissions: this.permissions,
|
||||
enabled: this.enabled,
|
||||
commands: this.listCommands(),
|
||||
tools: this.tools.length,
|
||||
hooks: Array.from(this.hooks.keys()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN BUILDER - Fluent API for Creating Plugins
|
||||
// ============================================================================
|
||||
|
||||
export class PluginBuilder {
|
||||
private config: Partial<PluginConfig> = {}
|
||||
private commandHandlers: Map<string, CommandHandler> = new Map()
|
||||
private toolExtensions: ToolExtension[] = []
|
||||
private hookHandlers: Map<HookEvent, HookHandler[]> = new Map()
|
||||
private configValues: Map<string, any> = new Map()
|
||||
|
||||
name(name: string): this {
|
||||
this.config.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
version(version: string): this {
|
||||
this.config.version = version
|
||||
return this
|
||||
}
|
||||
|
||||
description(description: string): this {
|
||||
this.config.description = description
|
||||
return this
|
||||
}
|
||||
|
||||
author(author: string): this {
|
||||
this.config.author = author
|
||||
return this
|
||||
}
|
||||
|
||||
license(license: string): this {
|
||||
this.config.license = license
|
||||
return this
|
||||
}
|
||||
|
||||
repository(repository: string): this {
|
||||
this.config.repository = repository
|
||||
return this
|
||||
}
|
||||
|
||||
permissions(...permissions: Permission[]): this {
|
||||
this.config.permissions = permissions
|
||||
return this
|
||||
}
|
||||
|
||||
enabled(enabled: boolean): this {
|
||||
this.config.enabled = enabled
|
||||
return this
|
||||
}
|
||||
|
||||
command(name: string, handler: CommandHandler): this {
|
||||
this.commandHandlers.set(name, handler)
|
||||
return this
|
||||
}
|
||||
|
||||
tool(tool: ToolExtension): this {
|
||||
this.toolExtensions.push(tool)
|
||||
return this
|
||||
}
|
||||
|
||||
hook(event: HookEvent, handler: HookHandler): this {
|
||||
const existing = this.hookHandlers.get(event) || []
|
||||
existing.push(handler)
|
||||
this.hookHandlers.set(event, existing)
|
||||
return this
|
||||
}
|
||||
|
||||
config(key: string, value: any): this {
|
||||
this.configValues.set(key, value)
|
||||
return this
|
||||
}
|
||||
|
||||
onLoad(handler: () => Promise<void>): this {
|
||||
return this.hook('PluginLoad', handler as HookHandler)
|
||||
}
|
||||
|
||||
onUnload(handler: () => Promise<void>): this {
|
||||
return this.hook('PluginUnload', handler as HookHandler)
|
||||
}
|
||||
|
||||
onSessionStart(handler: HookHandler): this {
|
||||
return this.hook('SessionStart', handler)
|
||||
}
|
||||
|
||||
onSessionEnd(handler: HookHandler): this {
|
||||
return this.hook('SessionEnd', handler)
|
||||
}
|
||||
|
||||
onPreToolUse(handler: HookHandler): this {
|
||||
return this.hook('PreToolUse', handler)
|
||||
}
|
||||
|
||||
onPostToolUse(handler: HookHandler): this {
|
||||
return this.hook('PostToolUse', handler)
|
||||
}
|
||||
|
||||
onPreFileEdit(handler: HookHandler): this {
|
||||
return this.hook('PreFileEdit', handler)
|
||||
}
|
||||
|
||||
onPostFileEdit(handler: HookHandler): this {
|
||||
return this.hook('PostFileEdit', handler)
|
||||
}
|
||||
|
||||
build(security: SecurityManager, hookSystem: HookSystem): Plugin {
|
||||
if (!this.config.name || !this.config.version || !this.config.author) {
|
||||
throw new Error('Plugin must have name, version, and author')
|
||||
}
|
||||
|
||||
const plugin = new Plugin(
|
||||
this.config as PluginConfig,
|
||||
security,
|
||||
hookSystem
|
||||
)
|
||||
|
||||
// Register commands
|
||||
for (const [name, handler] of this.commandHandlers) {
|
||||
plugin.registerCommand(name, handler)
|
||||
}
|
||||
|
||||
// Register tools
|
||||
for (const tool of this.toolExtensions) {
|
||||
plugin.registerTool(tool)
|
||||
}
|
||||
|
||||
// Register hooks
|
||||
for (const [event, handlers] of this.hookHandlers) {
|
||||
for (const handler of handlers) {
|
||||
plugin.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Load config
|
||||
plugin.loadConfig(Object.fromEntries(this.configValues))
|
||||
|
||||
return plugin
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
export function createPlugin(
|
||||
config: PluginConfig,
|
||||
builder?: (plugin: PluginBuilder) => PluginBuilder
|
||||
): PluginBuilder {
|
||||
const pb = new PluginBuilder()
|
||||
|
||||
if (config.name) pb.name(config.name)
|
||||
if (config.version) pb.version(config.version)
|
||||
if (config.description) pb.description(config.description)
|
||||
if (config.author) pb.author(config.author)
|
||||
if (config.license) pb.license(config.license)
|
||||
if (config.repository) pb.repository(config.repository)
|
||||
if (config.permissions) pb.permissions(...config.permissions)
|
||||
if (config.enabled !== undefined) pb.enabled(config.enabled)
|
||||
|
||||
return builder ? builder(pb) : pb
|
||||
}
|
||||
|
||||
export function definePlugin(
|
||||
config: PluginConfig,
|
||||
definition: (pb: PluginBuilder) => void
|
||||
): PluginBuilder {
|
||||
const builder = new PluginBuilder()
|
||||
|
||||
// Set basic config
|
||||
if (config.name) builder.name(config.name)
|
||||
if (config.version) builder.version(config.version)
|
||||
if (config.description) builder.description(config.description)
|
||||
if (config.author) builder.author(config.author)
|
||||
if (config.license) builder.license(config.license)
|
||||
if (config.repository) builder.repository(config.repository)
|
||||
if (config.permissions) builder.permissions(...config.permissions)
|
||||
if (config.enabled !== undefined) builder.enabled(config.enabled)
|
||||
|
||||
// Apply definition
|
||||
definition(builder)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export default Plugin
|
||||
579
plugins/core/plugin-manager.ts
Normal file
579
plugins/core/plugin-manager.ts
Normal file
@@ -0,0 +1,579 @@
|
||||
/**
|
||||
* Claude Code Plugin Manager - Conduit-style Plugin System
|
||||
*
|
||||
* Features:
|
||||
* - GitHub-based plugin discovery
|
||||
* - Secure plugin installation with validation
|
||||
* - Version management and updates
|
||||
* - Dependency resolution
|
||||
* - Plugin lifecycle management
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { spawn } from 'child_process'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
export interface PluginMetadata {
|
||||
name: string
|
||||
version: string
|
||||
description: string
|
||||
author: string
|
||||
license?: string
|
||||
repository?: string
|
||||
homepage?: string
|
||||
keywords?: string[]
|
||||
claude: {
|
||||
minVersion?: string
|
||||
maxVersion?: string
|
||||
permissions: string[]
|
||||
hooks?: HookDefinition[]
|
||||
commands?: CommandDefinition[]
|
||||
skills?: SkillDefinition[]
|
||||
}
|
||||
dependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface HookDefinition {
|
||||
event: string
|
||||
handler: string
|
||||
priority?: number
|
||||
condition?: string
|
||||
}
|
||||
|
||||
export interface CommandDefinition {
|
||||
name: string
|
||||
description: string
|
||||
handler: string
|
||||
permissions?: string[]
|
||||
}
|
||||
|
||||
export interface SkillDefinition {
|
||||
name: string
|
||||
description: string
|
||||
file: string
|
||||
}
|
||||
|
||||
export interface InstalledPlugin {
|
||||
metadata: PluginMetadata
|
||||
installPath: string
|
||||
version: string
|
||||
installedAt: string
|
||||
lastUpdated: string
|
||||
scope: 'global' | 'project'
|
||||
projectPath?: string
|
||||
enabled: boolean
|
||||
integrity: string
|
||||
}
|
||||
|
||||
export interface MarketplaceSource {
|
||||
type: 'github' | 'directory' | 'npm'
|
||||
url?: string
|
||||
repo?: string
|
||||
path?: string
|
||||
lastUpdated?: string
|
||||
}
|
||||
|
||||
export interface PluginDiscoveryResult {
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
author: string
|
||||
source: string
|
||||
downloads?: number
|
||||
stars?: number
|
||||
updated: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN MANAGER CLASS
|
||||
// ============================================================================
|
||||
|
||||
export class PluginManager {
|
||||
private pluginsDir: string
|
||||
private cacheDir: string
|
||||
private marketplacesFile: string
|
||||
private installedFile: string
|
||||
private configDir: string
|
||||
|
||||
constructor(claudeDir: string = path.join(process.env.HOME || '', '.claude')) {
|
||||
this.configDir = claudeDir
|
||||
this.pluginsDir = path.join(claudeDir, 'plugins')
|
||||
this.cacheDir = path.join(this.pluginsDir, 'cache')
|
||||
this.marketplacesFile = path.join(this.pluginsDir, 'known_marketplaces.json')
|
||||
this.installedFile = path.join(this.pluginsDir, 'installed_plugins.json')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.ensureDirectories()
|
||||
await this.loadMarketplaces()
|
||||
await this.loadInstalledPlugins()
|
||||
}
|
||||
|
||||
private async ensureDirectories(): Promise<void> {
|
||||
const dirs = [
|
||||
this.pluginsDir,
|
||||
this.cacheDir,
|
||||
path.join(this.pluginsDir, 'marketplaces'),
|
||||
path.join(this.pluginsDir, 'tmp'),
|
||||
]
|
||||
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
} catch (error) {
|
||||
// Directory might already exist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MARKETPLACE MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
async addMarketplace(name: string, source: MarketplaceSource): Promise<void> {
|
||||
const marketplaces = await this.loadMarketplaces()
|
||||
|
||||
marketplaces[name] = {
|
||||
source,
|
||||
installLocation: path.join(this.pluginsDir, 'marketplaces', name),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
this.marketplacesFile,
|
||||
JSON.stringify(marketplaces, null, 2)
|
||||
)
|
||||
|
||||
// Clone/download marketplace if it's a GitHub repo
|
||||
if (source.type === 'github' && source.repo) {
|
||||
await this.cloneRepository(
|
||||
`https://github.com/${source.repo}.git`,
|
||||
path.join(this.pluginsDir, 'marketplaces', name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async loadMarketplaces(): Promise<Record<string, any>> {
|
||||
try {
|
||||
const content = await fs.readFile(this.marketplacesFile, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN DISCOVERY
|
||||
// ============================================================================
|
||||
|
||||
async discoverPlugins(query?: string): Promise<PluginDiscoveryResult[]> {
|
||||
const marketplaces = await this.loadMarketplaces()
|
||||
const results: PluginDiscoveryResult[] = []
|
||||
|
||||
for (const [name, marketplace]: Object.entries(marketplaces)) {
|
||||
const mp = marketplace as any
|
||||
const pluginsPath = path.join(mp.installLocation, 'plugins')
|
||||
|
||||
try {
|
||||
const pluginDirs = await fs.readdir(pluginsPath)
|
||||
|
||||
for (const pluginDir of pluginDirs) {
|
||||
const pluginJsonPath = path.join(
|
||||
pluginsPath,
|
||||
pluginDir,
|
||||
'.claude-plugin',
|
||||
'plugin.json'
|
||||
)
|
||||
|
||||
try {
|
||||
const metadata = JSON.parse(
|
||||
await fs.readFile(pluginJsonPath, 'utf-8')
|
||||
) as PluginMetadata
|
||||
|
||||
// Filter by query if provided
|
||||
if (
|
||||
query &&
|
||||
!metadata.name.toLowerCase().includes(query.toLowerCase()) &&
|
||||
!metadata.description?.toLowerCase().includes(query.toLowerCase()) &&
|
||||
!metadata.keywords?.some((k) =>
|
||||
k.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
results.push({
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
version: metadata.version,
|
||||
author: metadata.author,
|
||||
source: name,
|
||||
updated: new Date().toISOString(),
|
||||
})
|
||||
} catch {
|
||||
// Skip invalid plugins
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Marketplace might not have plugins directory
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN INSTALLATION
|
||||
// ============================================================================
|
||||
|
||||
async installPlugin(
|
||||
marketplace: string,
|
||||
pluginName: string,
|
||||
scope: 'global' | 'project' = 'global'
|
||||
): Promise<InstalledPlugin> {
|
||||
const marketplaces = await this.loadMarketplaces()
|
||||
const mp = marketplaces[marketplace] as any
|
||||
|
||||
if (!mp) {
|
||||
throw new Error(`Marketplace "${marketplace}" not found`)
|
||||
}
|
||||
|
||||
const sourcePath = path.join(mp.installLocation, 'plugins', pluginName)
|
||||
const pluginJsonPath = path.join(sourcePath, '.claude-plugin', 'plugin.json')
|
||||
|
||||
// Read plugin metadata
|
||||
const metadata: PluginMetadata = JSON.parse(
|
||||
await fs.readFile(pluginJsonPath, 'utf-8')
|
||||
)
|
||||
|
||||
// Validate permissions
|
||||
await this.validatePermissions(metadata.claude.permissions)
|
||||
|
||||
// Calculate integrity hash
|
||||
const integrity = await this.calculateIntegrity(sourcePath)
|
||||
|
||||
// Install to cache
|
||||
const versionedPath = path.join(
|
||||
this.cacheDir,
|
||||
marketplace,
|
||||
`${pluginName}-${metadata.version}`
|
||||
)
|
||||
await fs.mkdir(versionedPath, { recursive: true })
|
||||
|
||||
await this.copyDirectory(sourcePath, versionedPath)
|
||||
|
||||
const installedPlugin: InstalledPlugin = {
|
||||
metadata,
|
||||
installPath: versionedPath,
|
||||
version: metadata.version,
|
||||
installedAt: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
scope,
|
||||
enabled: true,
|
||||
integrity,
|
||||
}
|
||||
|
||||
// Register plugin
|
||||
await this.registerPlugin(installedPlugin)
|
||||
|
||||
// Run install script if present
|
||||
const installScript = path.join(sourcePath, 'install.sh')
|
||||
try {
|
||||
await this.runScript(installScript, versionedPath)
|
||||
} catch {
|
||||
// No install script or failed
|
||||
}
|
||||
|
||||
return installedPlugin
|
||||
}
|
||||
|
||||
async installFromGitHub(
|
||||
repo: string,
|
||||
scope: 'global' | 'project' = 'global'
|
||||
): Promise<InstalledPlugin> {
|
||||
const [owner, name] = repo.split('/')
|
||||
const tempDir = path.join(this.pluginsDir, 'tmp', `${name}-${Date.now()}`)
|
||||
|
||||
// Clone repository
|
||||
await this.cloneRepository(`https://github.com/${repo}.git`, tempDir)
|
||||
|
||||
// Read plugin metadata
|
||||
const pluginJsonPath = path.join(tempDir, '.claude-plugin', 'plugin.json')
|
||||
const metadata: PluginMetadata = JSON.parse(
|
||||
await fs.readFile(pluginJsonPath, 'utf-8')
|
||||
)
|
||||
|
||||
// Validate and install
|
||||
const integrity = await this.calculateIntegrity(tempDir)
|
||||
const versionedPath = path.join(
|
||||
this.cacheDir,
|
||||
'github',
|
||||
`${name}-${metadata.version}`
|
||||
)
|
||||
await fs.mkdir(versionedPath, { recursive: true })
|
||||
await this.copyDirectory(tempDir, versionedPath)
|
||||
|
||||
const installedPlugin: InstalledPlugin = {
|
||||
metadata,
|
||||
installPath: versionedPath,
|
||||
version: metadata.version,
|
||||
installedAt: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
scope,
|
||||
enabled: true,
|
||||
integrity,
|
||||
}
|
||||
|
||||
await this.registerPlugin(installedPlugin)
|
||||
|
||||
// Cleanup temp dir
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
|
||||
return installedPlugin
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
async uninstallPlugin(name: string, marketplace?: string): Promise<void> {
|
||||
const installed = await this.loadInstalledPlugins()
|
||||
|
||||
const key = marketplace ? `${name}@${marketplace}` : name
|
||||
|
||||
if (!installed[key]) {
|
||||
throw new Error(`Plugin "${name}" is not installed`)
|
||||
}
|
||||
|
||||
const plugin = installed[key][0] as InstalledPlugin
|
||||
|
||||
// Run uninstall script if present
|
||||
const uninstallScript = path.join(plugin.installPath, 'uninstall.sh')
|
||||
try {
|
||||
await this.runScript(uninstallScript, plugin.installPath)
|
||||
} catch {
|
||||
// No uninstall script or failed
|
||||
}
|
||||
|
||||
// Remove from cache
|
||||
await fs.rm(plugin.installPath, { recursive: true, force: true })
|
||||
|
||||
// Unregister
|
||||
delete installed[key]
|
||||
await fs.writeFile(
|
||||
this.installedFile,
|
||||
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
async enablePlugin(name: string, marketplace?: string): Promise<void> {
|
||||
const installed = await this.loadInstalledPlugins()
|
||||
const key = marketplace ? `${name}@${marketplace}` : name
|
||||
|
||||
if (installed[key]) {
|
||||
installed[key][0].enabled = true
|
||||
await fs.writeFile(
|
||||
this.installedFile,
|
||||
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async disablePlugin(name: string, marketplace?: string): Promise<void> {
|
||||
const installed = await this.loadInstalledPlugins()
|
||||
const key = marketplace ? `${name}@${marketplace}` : name
|
||||
|
||||
if (installed[key]) {
|
||||
installed[key][0].enabled = false
|
||||
await fs.writeFile(
|
||||
this.installedFile,
|
||||
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async updatePlugin(name: string, marketplace?: string): Promise<void> {
|
||||
const installed = await this.loadInstalledPlugins()
|
||||
const key = marketplace ? `${name}@${marketplace}` : name
|
||||
|
||||
if (!installed[key]) {
|
||||
throw new Error(`Plugin "${name}" is not installed`)
|
||||
}
|
||||
|
||||
const plugin = installed[key][0] as InstalledPlugin
|
||||
|
||||
// Reinstall to update
|
||||
if (plugin.metadata.repository) {
|
||||
await this.installFromGitHub(
|
||||
plugin.metadata.repository.replace('https://github.com/', ''),
|
||||
plugin.scope
|
||||
)
|
||||
} else {
|
||||
// Reinstall from marketplace
|
||||
// Implementation depends on marketplace type
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN LOADING
|
||||
// ============================================================================
|
||||
|
||||
async loadInstalledPlugins(): Promise<Record<string, InstalledPlugin[]>> {
|
||||
try {
|
||||
const content = await fs.readFile(this.installedFile, 'utf-8')
|
||||
const data = JSON.parse(content)
|
||||
return data.plugins || {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
async getEnabledPlugins(): Promise<InstalledPlugin[]> {
|
||||
const installed = await this.loadInstalledPlugins()
|
||||
const enabled: InstalledPlugin[] = []
|
||||
|
||||
for (const plugins of Object.values(installed)) {
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.enabled) {
|
||||
enabled.push(plugin as InstalledPlugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SECURITY
|
||||
// ============================================================================
|
||||
|
||||
private async validatePermissions(permissions: string[]): Promise<void> {
|
||||
const allowedPermissions = [
|
||||
'read:files',
|
||||
'write:files',
|
||||
'execute:commands',
|
||||
'network:request',
|
||||
'read:config',
|
||||
'write:config',
|
||||
'hook:events',
|
||||
]
|
||||
|
||||
for (const perm of permissions) {
|
||||
if (!allowedPermissions.includes(perm)) {
|
||||
throw new Error(`Unknown permission: ${perm}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async calculateIntegrity(dirPath: string): Promise<string> {
|
||||
const hash = createHash('sha256')
|
||||
const files = await this.getAllFiles(dirPath)
|
||||
|
||||
for (const file of files.sort()) {
|
||||
const content = await fs.readFile(file)
|
||||
hash.update(content)
|
||||
}
|
||||
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
private async getAllFiles(dirPath: string): Promise<string[]> {
|
||||
const files: string[] = []
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await this.getAllFiles(fullPath)))
|
||||
} else {
|
||||
files.push(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
private async registerPlugin(plugin: InstalledPlugin): Promise<void> {
|
||||
const installed = await this.loadInstalledPlugins()
|
||||
const key = `${plugin.metadata.name}@${plugin.installPath.split('/').slice(-2).join('/')}`
|
||||
|
||||
if (!installed[key]) {
|
||||
installed[key] = []
|
||||
}
|
||||
|
||||
installed[key].push(plugin)
|
||||
|
||||
await fs.writeFile(
|
||||
this.installedFile,
|
||||
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
private async cloneRepository(repoUrl: string, targetPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const git = spawn('git', ['clone', '--depth', '1', repoUrl, targetPath], {
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
git.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`Git clone failed with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async copyDirectory(source: string, target: string): Promise<void> {
|
||||
await fs.mkdir(target, { recursive: true })
|
||||
const entries = await fs.readdir(source, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(source, entry.name)
|
||||
const destPath = path.join(target, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await this.copyDirectory(srcPath, destPath)
|
||||
} else {
|
||||
await fs.copyFile(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async runScript(scriptPath: string, cwd: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shell = spawn('bash', [scriptPath], {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
shell.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`Script failed with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export default PluginManager
|
||||
533
plugins/core/security.ts
Normal file
533
plugins/core/security.ts
Normal file
@@ -0,0 +1,533 @@
|
||||
/**
|
||||
* Claude Code Plugin Security System
|
||||
*
|
||||
* Features:
|
||||
* - Permission validation
|
||||
* - File system sandboxing
|
||||
* - Command execution validation
|
||||
* - Network access control
|
||||
* - Resource limits
|
||||
* - Code injection prevention
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
export type Permission =
|
||||
| 'read:files'
|
||||
| 'write:files'
|
||||
| 'execute:commands'
|
||||
| 'network:request'
|
||||
| 'read:config'
|
||||
| 'write:config'
|
||||
| 'hook:events'
|
||||
| 'read:secrets'
|
||||
|
||||
export interface SecurityPolicy {
|
||||
allowedPaths: string[]
|
||||
deniedPaths: string[]
|
||||
allowedCommands: string[]
|
||||
deniedCommands: string[]
|
||||
allowedDomains: string[]
|
||||
deniedDomains: string[]
|
||||
maxFileSize: number
|
||||
maxExecutionTime: number
|
||||
requireCodeSigning: boolean
|
||||
}
|
||||
|
||||
export interface SecurityContext {
|
||||
pluginName: string
|
||||
permissions: Permission[]
|
||||
workingDirectory: string
|
||||
startTime: number
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
allowed: boolean
|
||||
reason?: string
|
||||
modifiedValue?: any
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SECURITY MANAGER CLASS
|
||||
// ============================================================================
|
||||
|
||||
export class SecurityManager {
|
||||
private policy: SecurityPolicy
|
||||
private contexts: Map<string, SecurityContext> = new Map()
|
||||
private auditLog: AuditLog[] = []
|
||||
|
||||
constructor(policy?: Partial<SecurityPolicy>) {
|
||||
this.policy = {
|
||||
allowedPaths: [],
|
||||
deniedPaths: [],
|
||||
allowedCommands: [],
|
||||
deniedCommands: ['rm -rf /', 'format', 'mkfs'],
|
||||
allowedDomains: [],
|
||||
deniedDomains: [],
|
||||
maxFileSize: 100 * 1024 * 1024, // 100MB
|
||||
maxExecutionTime: 30000, // 30 seconds
|
||||
requireCodeSigning: false,
|
||||
...policy,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONTEXT MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
createContext(pluginName: string, permissions: Permission[]): SecurityContext {
|
||||
const context: SecurityContext = {
|
||||
pluginName,
|
||||
permissions,
|
||||
workingDirectory: process.cwd(),
|
||||
startTime: Date.now(),
|
||||
}
|
||||
|
||||
this.contexts.set(pluginName, context)
|
||||
return context
|
||||
}
|
||||
|
||||
getContext(pluginName: string): SecurityContext | undefined {
|
||||
return this.contexts.get(pluginName)
|
||||
}
|
||||
|
||||
removeContext(pluginName: string): void {
|
||||
this.contexts.delete(pluginName)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PERMISSION VALIDATION
|
||||
// ============================================================================
|
||||
|
||||
hasPermission(pluginName: string, permission: Permission): boolean {
|
||||
const context = this.getContext(pluginName)
|
||||
if (!context) return false
|
||||
|
||||
return context.permissions.includes(permission)
|
||||
}
|
||||
|
||||
validatePermissions(
|
||||
pluginName: string,
|
||||
requiredPermissions: Permission[]
|
||||
): ValidationResult {
|
||||
const context = this.getContext(pluginName)
|
||||
|
||||
if (!context) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Plugin context not found',
|
||||
}
|
||||
}
|
||||
|
||||
const missing = requiredPermissions.filter(
|
||||
(perm) => !context.permissions.includes(perm)
|
||||
)
|
||||
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Missing permissions: ${missing.join(', ')}`,
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FILE SYSTEM VALIDATION
|
||||
// ============================================================================
|
||||
|
||||
async validateFileAccess(
|
||||
pluginName: string,
|
||||
filePath: string,
|
||||
mode: 'read' | 'write'
|
||||
): Promise<ValidationResult> {
|
||||
const permission = mode === 'read' ? 'read:files' : 'write:files'
|
||||
|
||||
if (!this.hasPermission(pluginName, permission)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Missing permission: ${permission}`,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(filePath)
|
||||
|
||||
// Check denied paths first
|
||||
for (const denied of this.policy.deniedPaths) {
|
||||
if (resolvedPath.startsWith(path.resolve(denied))) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Access denied to path: ${filePath}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If allowed paths are specified, check against them
|
||||
if (this.policy.allowedPaths.length > 0) {
|
||||
const allowed = this.policy.allowedPaths.some((allowedPath) =>
|
||||
resolvedPath.startsWith(path.resolve(allowedPath))
|
||||
)
|
||||
|
||||
if (!allowed) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Path not in allowed list: ${filePath}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check file size for writes
|
||||
if (mode === 'write') {
|
||||
try {
|
||||
const stats = await fs.stat(resolvedPath)
|
||||
if (stats.size > this.policy.maxFileSize) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `File too large: ${stats.size} bytes`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist yet, that's fine for writes
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMMAND VALIDATION
|
||||
// ============================================================================
|
||||
|
||||
validateCommand(pluginName: string, command: string): ValidationResult {
|
||||
if (!this.hasPermission(pluginName, 'execute:commands')) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Missing permission: execute:commands',
|
||||
}
|
||||
}
|
||||
|
||||
// Check denied commands
|
||||
for (const denied of this.policy.deniedCommands) {
|
||||
if (command.includes(denied)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Command contains denied pattern: ${denied}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dangerous patterns
|
||||
const dangerousPatterns = [
|
||||
/\brm\s+-rf\s+\//,
|
||||
/\bformat\s+[a-z]:/i,
|
||||
/\bdel\s+\/[sq]/i,
|
||||
/\>\s*\/dev\/[a-z]+/,
|
||||
/\|.*\brm\b/,
|
||||
/&&.*\brm\b/,
|
||||
/;.*\brm\b/,
|
||||
]
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(command)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Command contains dangerous pattern',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NETWORK VALIDATION
|
||||
// ============================================================================
|
||||
|
||||
validateNetworkRequest(pluginName: string, url: string): ValidationResult {
|
||||
if (!this.hasPermission(pluginName, 'network:request')) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Missing permission: network:request',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
const domain = urlObj.hostname
|
||||
|
||||
// Check denied domains
|
||||
for (const denied of this.policy.deniedDomains) {
|
||||
if (domain === denied || domain.endsWith(`.${denied}`)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Access denied to domain: ${domain}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If allowed domains are specified, check against them
|
||||
if (this.policy.allowedDomains.length > 0) {
|
||||
const allowed = this.policy.allowedDomains.some(
|
||||
(allowed) => domain === allowed || domain.endsWith(`.${allowed}`)
|
||||
)
|
||||
|
||||
if (!allowed) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Domain not in allowed list: ${domain}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Invalid URL',
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CODE INJECTION PREVENTION
|
||||
// ============================================================================
|
||||
|
||||
sanitizeInput(input: string): string {
|
||||
// Remove potential code injection patterns
|
||||
return input
|
||||
.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/on\w+\s*=/gi, '')
|
||||
.replace(/\${.*?}/g, '') // Template literals
|
||||
.replace(/`.*?`/g, '') // Backtick expressions
|
||||
.replace(/eval\s*\(/gi, '')
|
||||
.replace(/Function\s*\(/gi, '')
|
||||
.replace(/setTimeout\s*\(/gi, '')
|
||||
.replace(/setInterval\s*\(/gi, '')
|
||||
}
|
||||
|
||||
validateScriptCode(code: string): ValidationResult {
|
||||
const dangerousPatterns = [
|
||||
/eval\s*\(/,
|
||||
/Function\s*\(/,
|
||||
/require\s*\(\s*['"`]fs['"`]\s*\)/,
|
||||
/require\s*\(\s*['"`]child_process['"`]\s*\)/,
|
||||
/process\s*\.\s*exit/,
|
||||
/\.\.\//, // Path traversal
|
||||
/~\//, // Home directory access
|
||||
]
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(code)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Code contains dangerous pattern: ${pattern.source}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INTEGRITY CHECKING
|
||||
// ============================================================================
|
||||
|
||||
async calculateFileIntegrity(filePath: string): Promise<string> {
|
||||
const content = await fs.readFile(filePath)
|
||||
return createHash('sha256').update(content).digest('hex')
|
||||
}
|
||||
|
||||
async verifyPluginIntegrity(
|
||||
pluginPath: string,
|
||||
expectedIntegrity: string
|
||||
): Promise<boolean> {
|
||||
const actualIntegrity = await this.calculateDirectoryIntegrity(pluginPath)
|
||||
return actualIntegrity === expectedIntegrity
|
||||
}
|
||||
|
||||
async calculateDirectoryIntegrity(dirPath: string): Promise<string> {
|
||||
const hash = createHash('sha256')
|
||||
const files = await this.getAllFiles(dirPath)
|
||||
|
||||
for (const file of files.sort()) {
|
||||
const content = await fs.readFile(file)
|
||||
hash.update(content)
|
||||
}
|
||||
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
private async getAllFiles(dirPath: string): Promise<string[]> {
|
||||
const files: string[] = []
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await this.getAllFiles(fullPath)))
|
||||
} else {
|
||||
files.push(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUDIT LOGGING
|
||||
// ============================================================================
|
||||
|
||||
logAccess(
|
||||
pluginName: string,
|
||||
action: string,
|
||||
resource: string,
|
||||
allowed: boolean
|
||||
): void {
|
||||
const entry: AuditLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
pluginName,
|
||||
action,
|
||||
resource,
|
||||
allowed,
|
||||
}
|
||||
|
||||
this.auditLog.push(entry)
|
||||
|
||||
// Keep only last 1000 entries
|
||||
if (this.auditLog.length > 1000) {
|
||||
this.auditLog = this.auditLog.slice(-1000)
|
||||
}
|
||||
}
|
||||
|
||||
getAuditLog(pluginName?: string): AuditLog[] {
|
||||
if (pluginName) {
|
||||
return this.auditLog.filter((entry) => entry.pluginName === pluginName)
|
||||
}
|
||||
return [...this.auditLog]
|
||||
}
|
||||
|
||||
clearAuditLog(): void {
|
||||
this.auditLog = []
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
interface AuditLog {
|
||||
timestamp: string
|
||||
pluginName: string
|
||||
action: string
|
||||
resource: string
|
||||
allowed: boolean
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SANDBOX CLASS - Isolated Execution Environment
|
||||
// ============================================================================
|
||||
|
||||
export class Sandbox {
|
||||
private security: SecurityManager
|
||||
private context: SecurityContext
|
||||
|
||||
constructor(security: SecurityManager, context: SecurityContext) {
|
||||
this.security = security
|
||||
this.context = context
|
||||
}
|
||||
|
||||
async readFile(filePath: string): Promise<string> {
|
||||
const validation = await this.security.validateFileAccess(
|
||||
this.context.pluginName,
|
||||
filePath,
|
||||
'read'
|
||||
)
|
||||
|
||||
if (!validation.allowed) {
|
||||
this.security.logAccess(
|
||||
this.context.pluginName,
|
||||
'read:file',
|
||||
filePath,
|
||||
false
|
||||
)
|
||||
throw new Error(validation.reason)
|
||||
}
|
||||
|
||||
this.security.logAccess(
|
||||
this.context.pluginName,
|
||||
'read:file',
|
||||
filePath,
|
||||
true
|
||||
)
|
||||
|
||||
return await fs.readFile(filePath, 'utf-8')
|
||||
}
|
||||
|
||||
async writeFile(filePath: string, content: string): Promise<void> {
|
||||
const validation = await this.security.validateFileAccess(
|
||||
this.context.pluginName,
|
||||
filePath,
|
||||
'write'
|
||||
)
|
||||
|
||||
if (!validation.allowed) {
|
||||
this.security.logAccess(
|
||||
this.context.pluginName,
|
||||
'write:file',
|
||||
filePath,
|
||||
false
|
||||
)
|
||||
throw new Error(validation.reason)
|
||||
}
|
||||
|
||||
this.security.logAccess(
|
||||
this.context.pluginName,
|
||||
'write:file',
|
||||
filePath,
|
||||
true
|
||||
)
|
||||
|
||||
await fs.writeFile(filePath, content, 'utf-8')
|
||||
}
|
||||
|
||||
executeCommand(command: string): Promise<string> {
|
||||
const validation = this.security.validateCommand(
|
||||
this.context.pluginName,
|
||||
command
|
||||
)
|
||||
|
||||
if (!validation.allowed) {
|
||||
this.security.logAccess(
|
||||
this.context.pluginName,
|
||||
'execute:command',
|
||||
command,
|
||||
false
|
||||
)
|
||||
throw new Error(validation.reason)
|
||||
}
|
||||
|
||||
this.security.logAccess(
|
||||
this.context.pluginName,
|
||||
'execute:command',
|
||||
command,
|
||||
true
|
||||
)
|
||||
|
||||
// Command execution would be implemented here
|
||||
return Promise.resolve('')
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export default SecurityManager
|
||||
Reference in New Issue
Block a user