mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-07 23:23:08 +08:00
Major OpenCode integration overhaul: - llms.txt: Comprehensive OpenCode documentation for LLMs (642 lines) - .opencode/plugins/ecc-hooks.ts: All Claude Code hooks translated to OpenCode's plugin system - .opencode/tools/*.ts: 3 custom tools (run-tests, check-coverage, security-audit) - .opencode/commands/*.md: All 24 commands in OpenCode format - .opencode/package.json: npm package structure for opencode-ecc - .opencode/index.ts: Main plugin entry point - Delete incorrect LIMITATIONS.md (hooks ARE supported via plugins) - Rewrite MIGRATION.md with correct hook event mapping - Update README.md OpenCode section to show full feature parity OpenCode has 20+ events vs Claude Code's 3 phases: - PreToolUse → tool.execute.before - PostToolUse → tool.execute.after - Stop → session.idle - SessionStart → session.created - SessionEnd → session.deleted - Plus: file.edited, file.watcher.updated, permission.asked, todo.updated - 12 agents: Full parity - 24 commands: Full parity (+1 from original 23) - 16 skills: Full parity - Hooks: OpenCode has MORE (20+ events vs 3 phases) - Custom Tools: 3 native OpenCode tools The OpenCode configuration can now be: 1. Used directly: cd everything-claude-code && opencode 2. Installed via npm: npm install opencode-ecc
290 lines
8.8 KiB
TypeScript
290 lines
8.8 KiB
TypeScript
/**
|
|
* Everything Claude Code (ECC) Plugin Hooks for OpenCode
|
|
*
|
|
* This plugin translates Claude Code hooks to OpenCode's plugin system.
|
|
* OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ events
|
|
* compared to Claude Code's 3 phases (PreToolUse, PostToolUse, Stop).
|
|
*
|
|
* Hook Event Mapping:
|
|
* - PreToolUse → tool.execute.before
|
|
* - PostToolUse → tool.execute.after
|
|
* - Stop → session.idle / session.status
|
|
* - SessionStart → session.created
|
|
* - SessionEnd → session.deleted
|
|
*/
|
|
|
|
import type { PluginContext } from "@opencode-ai/plugin"
|
|
|
|
export const ECCHooksPlugin = async ({
|
|
project,
|
|
client,
|
|
$,
|
|
directory,
|
|
worktree,
|
|
}: PluginContext) => {
|
|
// Track files edited in current session for console.log audit
|
|
const editedFiles = new Set<string>()
|
|
|
|
return {
|
|
/**
|
|
* Prettier Auto-Format Hook
|
|
* Equivalent to Claude Code PostToolUse hook for prettier
|
|
*
|
|
* Triggers: After any JS/TS/JSX/TSX file is edited
|
|
* Action: Runs prettier --write on the file
|
|
*/
|
|
"file.edited": async (event: { path: string }) => {
|
|
// Track edited files for console.log audit
|
|
editedFiles.add(event.path)
|
|
|
|
// Auto-format JS/TS files
|
|
if (event.path.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
try {
|
|
await $`prettier --write ${event.path} 2>/dev/null`
|
|
client.app.log("info", `[ECC] Formatted: ${event.path}`)
|
|
} catch {
|
|
// Prettier not installed or failed - silently continue
|
|
}
|
|
}
|
|
|
|
// Console.log warning check
|
|
if (event.path.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
try {
|
|
const result = await $`grep -n "console\\.log" ${event.path} 2>/dev/null`.text()
|
|
if (result.trim()) {
|
|
const lines = result.trim().split("\n").length
|
|
client.app.log(
|
|
"warn",
|
|
`[ECC] console.log found in ${event.path} (${lines} occurrence${lines > 1 ? "s" : ""})`
|
|
)
|
|
}
|
|
} catch {
|
|
// No console.log found (grep returns non-zero) - this is good
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* TypeScript Check Hook
|
|
* Equivalent to Claude Code PostToolUse hook for tsc
|
|
*
|
|
* Triggers: After edit tool completes on .ts/.tsx files
|
|
* Action: Runs tsc --noEmit to check for type errors
|
|
*/
|
|
"tool.execute.after": async (
|
|
input: { tool: string; args?: { filePath?: string } },
|
|
output: unknown
|
|
) => {
|
|
// Check if a TypeScript file was edited
|
|
if (
|
|
input.tool === "edit" &&
|
|
input.args?.filePath?.match(/\.tsx?$/)
|
|
) {
|
|
try {
|
|
await $`npx tsc --noEmit 2>&1`
|
|
client.app.log("info", "[ECC] TypeScript check passed")
|
|
} catch (error: unknown) {
|
|
const err = error as { stdout?: string }
|
|
client.app.log("warn", "[ECC] TypeScript errors detected:")
|
|
if (err.stdout) {
|
|
// Log first few errors
|
|
const errors = err.stdout.split("\n").slice(0, 5)
|
|
errors.forEach((line: string) => client.app.log("warn", ` ${line}`))
|
|
}
|
|
}
|
|
}
|
|
|
|
// PR creation logging
|
|
if (input.tool === "bash" && input.args?.toString().includes("gh pr create")) {
|
|
client.app.log("info", "[ECC] PR created - check GitHub Actions status")
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pre-Tool Security Check
|
|
* Equivalent to Claude Code PreToolUse hook
|
|
*
|
|
* Triggers: Before tool execution
|
|
* Action: Warns about potential security issues
|
|
*/
|
|
"tool.execute.before": async (
|
|
input: { tool: string; args?: Record<string, unknown> }
|
|
) => {
|
|
// Git push review reminder
|
|
if (
|
|
input.tool === "bash" &&
|
|
input.args?.toString().includes("git push")
|
|
) {
|
|
client.app.log(
|
|
"info",
|
|
"[ECC] Remember to review changes before pushing: git diff origin/main...HEAD"
|
|
)
|
|
}
|
|
|
|
// Block creation of unnecessary documentation files
|
|
if (
|
|
input.tool === "write" &&
|
|
input.args?.filePath &&
|
|
typeof input.args.filePath === "string"
|
|
) {
|
|
const filePath = input.args.filePath
|
|
if (
|
|
filePath.match(/\.(md|txt)$/i) &&
|
|
!filePath.includes("README") &&
|
|
!filePath.includes("CHANGELOG") &&
|
|
!filePath.includes("LICENSE") &&
|
|
!filePath.includes("CONTRIBUTING")
|
|
) {
|
|
client.app.log(
|
|
"warn",
|
|
`[ECC] Creating ${filePath} - consider if this documentation is necessary`
|
|
)
|
|
}
|
|
}
|
|
|
|
// Long-running command reminder
|
|
if (input.tool === "bash") {
|
|
const cmd = String(input.args?.command || input.args || "")
|
|
if (
|
|
cmd.match(/^(npm|pnpm|yarn|bun)\s+(install|build|test|run)/) ||
|
|
cmd.match(/^cargo\s+(build|test|run)/) ||
|
|
cmd.match(/^go\s+(build|test|run)/)
|
|
) {
|
|
client.app.log(
|
|
"info",
|
|
"[ECC] Long-running command detected - consider using background execution"
|
|
)
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Session Created Hook
|
|
* Equivalent to Claude Code SessionStart hook
|
|
*
|
|
* Triggers: When a new session starts
|
|
* Action: Loads context and displays welcome message
|
|
*/
|
|
"session.created": async () => {
|
|
client.app.log("info", "[ECC] Session started - Everything Claude Code hooks active")
|
|
|
|
// Check for project-specific context files
|
|
try {
|
|
const hasClaudeMd = await $`test -f ${worktree}/CLAUDE.md && echo "yes"`.text()
|
|
if (hasClaudeMd.trim() === "yes") {
|
|
client.app.log("info", "[ECC] Found CLAUDE.md - loading project context")
|
|
}
|
|
} catch {
|
|
// No CLAUDE.md found
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Session Idle Hook
|
|
* Equivalent to Claude Code Stop hook
|
|
*
|
|
* Triggers: When session becomes idle (task completed)
|
|
* Action: Runs console.log audit on all edited files
|
|
*/
|
|
"session.idle": async () => {
|
|
if (editedFiles.size === 0) return
|
|
|
|
client.app.log("info", "[ECC] Session idle - running console.log audit")
|
|
|
|
let totalConsoleLogCount = 0
|
|
const filesWithConsoleLogs: string[] = []
|
|
|
|
for (const file of editedFiles) {
|
|
if (!file.match(/\.(ts|tsx|js|jsx)$/)) continue
|
|
|
|
try {
|
|
const result = await $`grep -c "console\\.log" ${file} 2>/dev/null`.text()
|
|
const count = parseInt(result.trim(), 10)
|
|
if (count > 0) {
|
|
totalConsoleLogCount += count
|
|
filesWithConsoleLogs.push(file)
|
|
}
|
|
} catch {
|
|
// No console.log found
|
|
}
|
|
}
|
|
|
|
if (totalConsoleLogCount > 0) {
|
|
client.app.log(
|
|
"warn",
|
|
`[ECC] Audit: ${totalConsoleLogCount} console.log statement(s) in ${filesWithConsoleLogs.length} file(s)`
|
|
)
|
|
filesWithConsoleLogs.forEach((f) =>
|
|
client.app.log("warn", ` - ${f}`)
|
|
)
|
|
client.app.log("warn", "[ECC] Remove console.log statements before committing")
|
|
} else {
|
|
client.app.log("info", "[ECC] Audit passed: No console.log statements found")
|
|
}
|
|
|
|
// Desktop notification (macOS)
|
|
try {
|
|
await $`osascript -e 'display notification "Task completed!" with title "OpenCode ECC"' 2>/dev/null`
|
|
} catch {
|
|
// Notification not supported or failed
|
|
}
|
|
|
|
// Clear tracked files for next task
|
|
editedFiles.clear()
|
|
},
|
|
|
|
/**
|
|
* Session Deleted Hook
|
|
* Equivalent to Claude Code SessionEnd hook
|
|
*
|
|
* Triggers: When session ends
|
|
* Action: Final cleanup and state saving
|
|
*/
|
|
"session.deleted": async () => {
|
|
client.app.log("info", "[ECC] Session ended - cleaning up")
|
|
editedFiles.clear()
|
|
},
|
|
|
|
/**
|
|
* File Watcher Hook
|
|
* OpenCode-only feature
|
|
*
|
|
* Triggers: When file system changes are detected
|
|
* Action: Updates tracking
|
|
*/
|
|
"file.watcher.updated": async (event: { path: string; type: string }) => {
|
|
if (event.type === "change" && event.path.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
editedFiles.add(event.path)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Permission Asked Hook
|
|
* OpenCode-only feature
|
|
*
|
|
* Triggers: When permission is requested
|
|
* Action: Logs for audit trail
|
|
*/
|
|
"permission.asked": async (event: { tool: string; args: unknown }) => {
|
|
client.app.log("info", `[ECC] Permission requested for: ${event.tool}`)
|
|
},
|
|
|
|
/**
|
|
* Todo Updated Hook
|
|
* OpenCode-only feature
|
|
*
|
|
* Triggers: When todo list is updated
|
|
* Action: Logs progress
|
|
*/
|
|
"todo.updated": async (event: { todos: Array<{ text: string; done: boolean }> }) => {
|
|
const completed = event.todos.filter((t) => t.done).length
|
|
const total = event.todos.length
|
|
if (total > 0) {
|
|
client.app.log("info", `[ECC] Progress: ${completed}/${total} tasks completed`)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
export default ECCHooksPlugin
|