From 35d53cc74a122a18a541f34faa5ab7fe30f233f5 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 14 Dec 2025 17:16:52 +0900 Subject: [PATCH] feat: add OmO config with build agent hiding and startup toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add configurable build agent hiding (omo_agent.disable_build) - Add startup-toast hook to show version on OpenCode startup - Fix auto-update-checker to respect version pinning 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/config/index.ts | 2 + src/config/schema.ts | 7 ++++ src/hooks/auto-update-checker/cache.ts | 47 +++++++++++++++++++----- src/hooks/auto-update-checker/checker.ts | 37 ++++++++++++++----- src/hooks/auto-update-checker/index.ts | 47 ++++++++++++++++++++---- src/hooks/auto-update-checker/types.ts | 5 +++ src/index.ts | 22 ++++------- 7 files changed, 125 insertions(+), 42 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 7fcd69e..158fd8f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -5,6 +5,7 @@ export { McpNameSchema, AgentNameSchema, HookNameSchema, + OmoAgentConfigSchema, } from "./schema" export type { @@ -14,4 +15,5 @@ export type { McpName, AgentName, HookName, + OmoAgentConfig, } from "./schema" diff --git a/src/config/schema.ts b/src/config/schema.ts index 2f882cd..d945ad2 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -53,6 +53,7 @@ export const HookNameSchema = z.enum([ "rules-injector", "background-notification", "auto-update-checker", + "startup-toast", "keyword-detector", "agent-usage-reminder", ]) @@ -93,6 +94,10 @@ export const ClaudeCodeConfigSchema = z.object({ hooks: z.boolean().optional(), }) +export const OmoAgentConfigSchema = z.object({ + disable_build: z.boolean().optional(), +}) + export const OhMyOpenCodeConfigSchema = z.object({ $schema: z.string().optional(), disabled_mcps: z.array(McpNameSchema).optional(), @@ -101,6 +106,7 @@ export const OhMyOpenCodeConfigSchema = z.object({ agents: AgentOverridesSchema.optional(), claude_code: ClaudeCodeConfigSchema.optional(), google_auth: z.boolean().optional(), + omo_agent: OmoAgentConfigSchema.optional(), }) export type OhMyOpenCodeConfig = z.infer @@ -108,5 +114,6 @@ export type AgentOverrideConfig = z.infer export type AgentOverrides = z.infer export type AgentName = z.infer export type HookName = z.infer +export type OmoAgentConfig = z.infer export { McpNameSchema, type McpName } from "../mcp/types" diff --git a/src/hooks/auto-update-checker/cache.ts b/src/hooks/auto-update-checker/cache.ts index 855843a..d199679 100644 --- a/src/hooks/auto-update-checker/cache.ts +++ b/src/hooks/auto-update-checker/cache.ts @@ -1,18 +1,47 @@ import * as fs from "node:fs" -import { VERSION_FILE } from "./constants" +import * as path from "node:path" +import { CACHE_DIR, PACKAGE_NAME } from "./constants" import { log } from "../../shared/logger" -export function invalidateCache(): boolean { +export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean { try { - if (fs.existsSync(VERSION_FILE)) { - fs.unlinkSync(VERSION_FILE) - log(`[auto-update-checker] Cache invalidated: ${VERSION_FILE}`) - return true + const pkgDir = path.join(CACHE_DIR, "node_modules", packageName) + const pkgJsonPath = path.join(CACHE_DIR, "package.json") + + let packageRemoved = false + let dependencyRemoved = false + + if (fs.existsSync(pkgDir)) { + fs.rmSync(pkgDir, { recursive: true, force: true }) + log(`[auto-update-checker] Package removed: ${pkgDir}`) + packageRemoved = true } - log("[auto-update-checker] Version file not found, nothing to invalidate") - return false + + if (fs.existsSync(pkgJsonPath)) { + const content = fs.readFileSync(pkgJsonPath, "utf-8") + const pkgJson = JSON.parse(content) + if (pkgJson.dependencies?.[packageName]) { + delete pkgJson.dependencies[packageName] + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) + log(`[auto-update-checker] Dependency removed from package.json: ${packageName}`) + dependencyRemoved = true + } + } + + if (!packageRemoved && !dependencyRemoved) { + log(`[auto-update-checker] Package not found, nothing to invalidate: ${packageName}`) + return false + } + + return true } catch (err) { - log("[auto-update-checker] Failed to invalidate cache:", err) + log("[auto-update-checker] Failed to invalidate package:", err) return false } } + +/** @deprecated Use invalidatePackage instead - this nukes ALL plugins */ +export function invalidateCache(): boolean { + log("[auto-update-checker] WARNING: invalidateCache is deprecated, use invalidatePackage") + return invalidatePackage() +} diff --git a/src/hooks/auto-update-checker/checker.ts b/src/hooks/auto-update-checker/checker.ts index 95babfa..1b2d6a1 100644 --- a/src/hooks/auto-update-checker/checker.ts +++ b/src/hooks/auto-update-checker/checker.ts @@ -33,7 +33,13 @@ export function isLocalDevMode(directory: string): boolean { return false } -export function findPluginEntry(directory: string): string | null { +export interface PluginEntryInfo { + entry: string + isPinned: boolean + pinnedVersion: string | null +} + +export function findPluginEntry(directory: string): PluginEntryInfo | null { const projectConfig = path.join(directory, ".opencode", "opencode.json") for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) { @@ -44,8 +50,13 @@ export function findPluginEntry(directory: string): string | null { const plugins = config.plugin ?? [] for (const entry of plugins) { - if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`)) { - return entry + if (entry === PACKAGE_NAME) { + return { entry, isPinned: false, pinnedVersion: null } + } + if (entry.startsWith(`${PACKAGE_NAME}@`)) { + const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1) + const isPinned = pinnedVersion !== "latest" + return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null } } } } catch { @@ -91,29 +102,35 @@ export async function getLatestVersion(): Promise { export async function checkForUpdate(directory: string): Promise { if (isLocalDevMode(directory)) { log("[auto-update-checker] Local dev mode detected, skipping update check") - return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true } + return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true, isPinned: false } } - const pluginEntry = findPluginEntry(directory) - if (!pluginEntry) { + const pluginInfo = findPluginEntry(directory) + if (!pluginInfo) { log("[auto-update-checker] Plugin not found in config") - return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false } + return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false } + } + + // Respect version pinning + if (pluginInfo.isPinned) { + log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`) + return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true } } const currentVersion = getCachedVersion() if (!currentVersion) { log("[auto-update-checker] No cached version found") - return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false } + return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false } } const latestVersion = await getLatestVersion() if (!latestVersion) { log("[auto-update-checker] Failed to fetch latest version") - return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false } + return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false } } const needsUpdate = currentVersion !== latestVersion log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`) - return { needsUpdate, currentVersion, latestVersion, isLocalDev: false } + return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false } } diff --git a/src/hooks/auto-update-checker/index.ts b/src/hooks/auto-update-checker/index.ts index 1fd2223..01e750f 100644 --- a/src/hooks/auto-update-checker/index.ts +++ b/src/hooks/auto-update-checker/index.ts @@ -1,10 +1,12 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { checkForUpdate } from "./checker" -import { invalidateCache } from "./cache" +import { checkForUpdate, getCachedVersion } from "./checker" +import { invalidatePackage } from "./cache" import { PACKAGE_NAME } from "./constants" import { log } from "../../shared/logger" +import type { AutoUpdateCheckerOptions } from "./types" -export function createAutoUpdateCheckerHook(ctx: PluginInput) { +export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) { + const { showStartupToast = true } = options let hasChecked = false return { @@ -22,21 +24,35 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput) { if (result.isLocalDev) { log("[auto-update-checker] Skipped: local development mode") + if (showStartupToast) { + await showVersionToast(ctx, getCachedVersion()) + } + return + } + + if (result.isPinned) { + log(`[auto-update-checker] Skipped: version pinned to ${result.currentVersion}`) + if (showStartupToast) { + await showVersionToast(ctx, result.currentVersion) + } return } if (!result.needsUpdate) { log("[auto-update-checker] No update needed") + if (showStartupToast) { + await showVersionToast(ctx, result.currentVersion) + } return } - invalidateCache() + invalidatePackage(PACKAGE_NAME) await ctx.client.tui .showToast({ body: { - title: `${PACKAGE_NAME} Update`, - message: `v${result.latestVersion} available (current: v${result.currentVersion}). Restart OpenCode to apply.`, + title: `OhMyOpenCode ${result.latestVersion}`, + message: `OpenCode is now on Steroids. oMoMoMoMo...\nv${result.latestVersion} available. Restart OpenCode to apply.`, variant: "info" as const, duration: 8000, }, @@ -51,6 +67,21 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput) { } } -export type { UpdateCheckResult } from "./types" +async function showVersionToast(ctx: PluginInput, version: string | null): Promise { + const displayVersion = version ?? "unknown" + await ctx.client.tui + .showToast({ + body: { + title: `OhMyOpenCode ${displayVersion}`, + message: "OpenCode is now on Steroids. oMoMoMoMo...", + variant: "info" as const, + duration: 5000, + }, + }) + .catch(() => {}) + log(`[auto-update-checker] Startup toast shown: v${displayVersion}`) +} + +export type { UpdateCheckResult, AutoUpdateCheckerOptions } from "./types" export { checkForUpdate } from "./checker" -export { invalidateCache } from "./cache" +export { invalidatePackage, invalidateCache } from "./cache" diff --git a/src/hooks/auto-update-checker/types.ts b/src/hooks/auto-update-checker/types.ts index a9a7b19..13055f8 100644 --- a/src/hooks/auto-update-checker/types.ts +++ b/src/hooks/auto-update-checker/types.ts @@ -19,4 +19,9 @@ export interface UpdateCheckResult { currentVersion: string | null latestVersion: string | null isLocalDev: boolean + isPinned: boolean +} + +export interface AutoUpdateCheckerOptions { + showStartupToast?: boolean } diff --git a/src/index.ts b/src/index.ts index 360043f..fd71051 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import type { Plugin } from "@opencode-ai/plugin"; -import { createBuiltinAgents, BUILD_AGENT_PROMPT_EXTENSION } from "./agents"; +import { createBuiltinAgents } from "./agents"; import { createTodoContinuationEnforcer, createContextWindowMonitorHook, @@ -203,7 +203,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { ? createRulesInjectorHook(ctx) : null; const autoUpdateChecker = isHookEnabled("auto-update-checker") - ? createAutoUpdateCheckerHook(ctx) + ? createAutoUpdateCheckerHook(ctx, { + showStartupToast: isHookEnabled("startup-toast"), + }) : null; const keywordDetector = isHookEnabled("keyword-detector") ? createKeywordDetectorHook() @@ -252,26 +254,16 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const userAgents = (pluginConfig.claude_code?.agents ?? true) ? loadUserAgents() : {}; const projectAgents = (pluginConfig.claude_code?.agents ?? true) ? loadProjectAgents() : {}; + const shouldHideBuild = pluginConfig.omo_agent?.disable_build !== false; + config.agent = { ...builtinAgents, ...userAgents, ...projectAgents, ...config.agent, + ...(shouldHideBuild ? { build: { mode: "subagent" } } : {}), }; - // Inject orchestration prompt to all non-subagent agents - // Subagents are delegated TO, so they don't need orchestration guidance - for (const [agentName, agentConfig] of Object.entries(config.agent ?? {})) { - if (agentConfig && agentConfig.mode !== "subagent") { - const existingPrompt = agentConfig.prompt || ""; - const userOverride = pluginConfig.agents?.[agentName as keyof typeof pluginConfig.agents]?.prompt || ""; - config.agent[agentName] = { - ...agentConfig, - prompt: existingPrompt + BUILD_AGENT_PROMPT_EXTENSION + userOverride, - }; - } - } - config.tools = { ...config.tools, };