diff --git a/README.md b/README.md index d8c41b9186..3aedd783e4 100644 --- a/README.md +++ b/README.md @@ -806,6 +806,47 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc` } ``` +### Plugin Compatibility + +Oh My OpenCode automatically detects and adapts when other OpenCode plugins are installed that provide similar functionality. + +**Auto-Detection**: When starting, oh-my-opencode scans your `opencode.json` for conflicting plugins: + +- **DCP plugins** (`opencode-dcp`, `@opencode/dcp`) - Dynamic Context Pruning +- **ELF plugins** (`opencode-elf`, `@opencode/elf`) - Context management +- **Compaction plugins** (`opencode-compaction`, `@opencode/compaction`) - Session compaction + +**Auto-Disable**: If conflicts are detected, oh-my-opencode automatically disables its own hooks that would interfere: + +``` +⚠️ oh-my-opencode: Plugin Conflict Detection +════════════════════════════════════════════════════════════ +Detected 2 plugin(s) that may conflict with oh-my-opencode: + • opencode-dcp + • opencode-elf + +Auto-disabling 5 oh-my-opencode hook(s) to prevent conflicts: + • anthropic-context-window-limit-recovery + • preemptive-compaction + • compaction-context-injector + • context-window-monitor + • directory-agents-injector +════════════════════════════════════════════════════════════ +``` + +**Manual Control**: Override auto-detection by explicitly configuring `disabled_hooks`: + +```json +{ + "disabled_hooks": [ + "preemptive-compaction", + "anthropic-context-window-limit-recovery" + ] +} +``` + +This ensures smooth coexistence with your existing plugin ecosystem without requiring manual configuration. + ### Google Auth **Recommended**: Use the external [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin. It provides multi-account load balancing, more models (including Claude via Antigravity), and active maintenance. See [Installation > Google Gemini](#google-gemini-antigravity-oauth). diff --git a/src/index.ts b/src/index.ts index bae5d0288e..7a7bcc57b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,17 +62,25 @@ import { import { BackgroundManager } from "./features/background-agent"; import { SkillMcpManager } from "./features/skill-mcp-manager"; import { type HookName } from "./config"; -import { log } from "./shared"; +import { log, detectConflictingPlugins, warnAboutConflicts } from "./shared"; import { loadPluginConfig } from "./plugin-config"; import { createModelCacheState, getModelLimit } from "./plugin-state"; import { createConfigHandler } from "./plugin-handlers"; const OhMyOpenCodePlugin: Plugin = async (ctx) => { - // Start background tmux check immediately startTmuxCheck(); const pluginConfig = loadPluginConfig(ctx.directory, ctx); - const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []); + + const conflictDetection = detectConflictingPlugins(ctx.directory); + const autoDisabledHooks = new Set(conflictDetection.hooksToDisable); + const userDisabledHooks = new Set(pluginConfig.disabled_hooks ?? []); + const disabledHooks = new Set([...autoDisabledHooks, ...userDisabledHooks]); + + if (conflictDetection.hasConflicts) { + warnAboutConflicts(conflictDetection.conflicts, conflictDetection.hooksToDisable); + } + const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName); const modelCacheState = createModelCacheState(); diff --git a/src/shared/index.ts b/src/shared/index.ts index 3c3f25e7fe..bd8e7de89f 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -19,3 +19,4 @@ export * from "./migration" export * from "./opencode-config-dir" export * from "./opencode-version" export * from "./permission-compat" +export * from "./plugin-compatibility" diff --git a/src/shared/plugin-compatibility.ts b/src/shared/plugin-compatibility.ts new file mode 100644 index 0000000000..7b9aec8a67 --- /dev/null +++ b/src/shared/plugin-compatibility.ts @@ -0,0 +1,160 @@ +import * as fs from "fs"; +import * as path from "path"; +import { log } from "./logger"; +import { getUserConfigDir } from "./config-path"; +import { parseJsonc } from "./jsonc-parser"; +import type { HookName } from "../config"; + +interface OpenCodeConfig { + plugin?: string[]; + [key: string]: unknown; +} + +/** + * Plugins that conflict with oh-my-opencode's built-in features + */ +const CONFLICTING_PLUGINS = { + // DCP (Dynamic Context Pruning) plugins + dcp: [ + "opencode-dcp", + "@opencode/dcp", + "opencode-dynamic-context-pruning", + ], + // ELF (External Language Framework / Context management) plugins + elf: ["opencode-elf", "@opencode/elf", "opencode-context"], + // Compaction plugins + compaction: [ + "opencode-compaction", + "@opencode/compaction", + "opencode-auto-compact", + ], +} as const; + +/** + * Hooks that should be auto-disabled when conflicting plugins are detected + */ +const HOOKS_TO_DISABLE_ON_CONFLICT: Record = { + dcp: [ + "anthropic-context-window-limit-recovery", + "preemptive-compaction", + "compaction-context-injector", + ], + elf: [ + "context-window-monitor", + "directory-agents-injector", + "directory-readme-injector", + ], + compaction: [ + "preemptive-compaction", + "anthropic-context-window-limit-recovery", + "compaction-context-injector", + ], +}; + +/** + * Loads the main OpenCode config to detect installed plugins + */ +function loadOpencodeConfig(directory: string): OpenCodeConfig { + const projectConfigPaths = [ + path.join(directory, "opencode.json"), + path.join(directory, "opencode.jsonc"), + ]; + + const userConfigPaths = [ + path.join(getUserConfigDir(), "opencode", "opencode.json"), + path.join(getUserConfigDir(), "opencode", "opencode.jsonc"), + ]; + + const paths = [...projectConfigPaths, ...userConfigPaths]; + + for (const configPath of paths) { + try { + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, "utf-8"); + const config = parseJsonc(content); + if (config.plugin && Array.isArray(config.plugin)) { + log(`Loaded OpenCode config from ${configPath}`, { + pluginCount: config.plugin.length, + }); + return config; + } + } + } catch (err) { + log(`Error loading OpenCode config from ${configPath}:`, err); + } + } + + return {}; +} + +/** + * Detects if any conflicting plugins are installed + */ +export function detectConflictingPlugins( + directory: string +): { + hasConflicts: boolean; + conflicts: string[]; + hooksToDisable: HookName[]; +} { + const config = loadOpencodeConfig(directory); + const installedPlugins = config.plugin ?? []; + + const conflicts: string[] = []; + const hooksToDisable = new Set(); + + for (const [type, pluginNames] of Object.entries(CONFLICTING_PLUGINS)) { + for (const pluginName of pluginNames) { + const hasPlugin = installedPlugins.some( + (p) => + p === pluginName || p.startsWith(`${pluginName}@`) || p.includes(pluginName) + ); + + if (hasPlugin) { + conflicts.push(pluginName); + const hooksForType = HOOKS_TO_DISABLE_ON_CONFLICT[type] ?? []; + hooksForType.forEach((hook) => hooksToDisable.add(hook)); + log(`Detected conflicting plugin: ${pluginName} (type: ${type})`); + } + } + } + + return { + hasConflicts: conflicts.length > 0, + conflicts, + hooksToDisable: Array.from(hooksToDisable), + }; +} + +/** + * Logs warning messages about plugin conflicts + */ +export function warnAboutConflicts(conflicts: string[], hooksToDisable: HookName[]): void { + if (conflicts.length === 0) return; + + console.warn("\n⚠️ oh-my-opencode: Plugin Conflict Detection"); + console.warn("═".repeat(60)); + console.warn( + `\nDetected ${conflicts.length} plugin(s) that may conflict with oh-my-opencode:` + ); + conflicts.forEach((plugin) => console.warn(` • ${plugin}`)); + + console.warn( + `\nAuto-disabling ${hooksToDisable.length} oh-my-opencode hook(s) to prevent conflicts:` + ); + hooksToDisable.forEach((hook) => console.warn(` • ${hook}`)); + + console.warn("\nTo manually control this behavior, add to your config:"); + console.warn(' ~/.config/opencode/oh-my-opencode.json'); + console.warn(' or'); + console.warn(' .opencode/oh-my-opencode.json'); + console.warn('\n {'); + console.warn(' "disabled_hooks": ['); + hooksToDisable.forEach((hook, i) => { + const comma = i < hooksToDisable.length - 1 ? "," : ""; + console.warn(` "${hook}"${comma}`); + }); + console.warn(' ]'); + console.warn(' }'); + console.warn("\n" + "═".repeat(60) + "\n"); +}