feat: add OmO config with build agent hiding and startup toast
- 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)
This commit is contained in:
@@ -5,6 +5,7 @@ export {
|
|||||||
McpNameSchema,
|
McpNameSchema,
|
||||||
AgentNameSchema,
|
AgentNameSchema,
|
||||||
HookNameSchema,
|
HookNameSchema,
|
||||||
|
OmoAgentConfigSchema,
|
||||||
} from "./schema"
|
} from "./schema"
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -14,4 +15,5 @@ export type {
|
|||||||
McpName,
|
McpName,
|
||||||
AgentName,
|
AgentName,
|
||||||
HookName,
|
HookName,
|
||||||
|
OmoAgentConfig,
|
||||||
} from "./schema"
|
} from "./schema"
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const HookNameSchema = z.enum([
|
|||||||
"rules-injector",
|
"rules-injector",
|
||||||
"background-notification",
|
"background-notification",
|
||||||
"auto-update-checker",
|
"auto-update-checker",
|
||||||
|
"startup-toast",
|
||||||
"keyword-detector",
|
"keyword-detector",
|
||||||
"agent-usage-reminder",
|
"agent-usage-reminder",
|
||||||
])
|
])
|
||||||
@@ -93,6 +94,10 @@ export const ClaudeCodeConfigSchema = z.object({
|
|||||||
hooks: z.boolean().optional(),
|
hooks: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const OmoAgentConfigSchema = z.object({
|
||||||
|
disable_build: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const OhMyOpenCodeConfigSchema = z.object({
|
export const OhMyOpenCodeConfigSchema = z.object({
|
||||||
$schema: z.string().optional(),
|
$schema: z.string().optional(),
|
||||||
disabled_mcps: z.array(McpNameSchema).optional(),
|
disabled_mcps: z.array(McpNameSchema).optional(),
|
||||||
@@ -101,6 +106,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
|||||||
agents: AgentOverridesSchema.optional(),
|
agents: AgentOverridesSchema.optional(),
|
||||||
claude_code: ClaudeCodeConfigSchema.optional(),
|
claude_code: ClaudeCodeConfigSchema.optional(),
|
||||||
google_auth: z.boolean().optional(),
|
google_auth: z.boolean().optional(),
|
||||||
|
omo_agent: OmoAgentConfigSchema.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
||||||
@@ -108,5 +114,6 @@ export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
|||||||
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
||||||
export type AgentName = z.infer<typeof AgentNameSchema>
|
export type AgentName = z.infer<typeof AgentNameSchema>
|
||||||
export type HookName = z.infer<typeof HookNameSchema>
|
export type HookName = z.infer<typeof HookNameSchema>
|
||||||
|
export type OmoAgentConfig = z.infer<typeof OmoAgentConfigSchema>
|
||||||
|
|
||||||
export { McpNameSchema, type McpName } from "../mcp/types"
|
export { McpNameSchema, type McpName } from "../mcp/types"
|
||||||
|
|||||||
@@ -1,18 +1,47 @@
|
|||||||
import * as fs from "node:fs"
|
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"
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
export function invalidateCache(): boolean {
|
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(VERSION_FILE)) {
|
const pkgDir = path.join(CACHE_DIR, "node_modules", packageName)
|
||||||
fs.unlinkSync(VERSION_FILE)
|
const pkgJsonPath = path.join(CACHE_DIR, "package.json")
|
||||||
log(`[auto-update-checker] Cache invalidated: ${VERSION_FILE}`)
|
|
||||||
return true
|
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) {
|
} catch (err) {
|
||||||
log("[auto-update-checker] Failed to invalidate cache:", err)
|
log("[auto-update-checker] Failed to invalidate package:", err)
|
||||||
return false
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ export function isLocalDevMode(directory: string): boolean {
|
|||||||
return false
|
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")
|
const projectConfig = path.join(directory, ".opencode", "opencode.json")
|
||||||
|
|
||||||
for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) {
|
for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) {
|
||||||
@@ -44,8 +50,13 @@ export function findPluginEntry(directory: string): string | null {
|
|||||||
const plugins = config.plugin ?? []
|
const plugins = config.plugin ?? []
|
||||||
|
|
||||||
for (const entry of plugins) {
|
for (const entry of plugins) {
|
||||||
if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`)) {
|
if (entry === PACKAGE_NAME) {
|
||||||
return entry
|
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 {
|
} catch {
|
||||||
@@ -91,29 +102,35 @@ export async function getLatestVersion(): Promise<string | null> {
|
|||||||
export async function checkForUpdate(directory: string): Promise<UpdateCheckResult> {
|
export async function checkForUpdate(directory: string): Promise<UpdateCheckResult> {
|
||||||
if (isLocalDevMode(directory)) {
|
if (isLocalDevMode(directory)) {
|
||||||
log("[auto-update-checker] Local dev mode detected, skipping update check")
|
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)
|
const pluginInfo = findPluginEntry(directory)
|
||||||
if (!pluginEntry) {
|
if (!pluginInfo) {
|
||||||
log("[auto-update-checker] Plugin not found in config")
|
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()
|
const currentVersion = getCachedVersion()
|
||||||
if (!currentVersion) {
|
if (!currentVersion) {
|
||||||
log("[auto-update-checker] No cached version found")
|
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()
|
const latestVersion = await getLatestVersion()
|
||||||
if (!latestVersion) {
|
if (!latestVersion) {
|
||||||
log("[auto-update-checker] Failed to fetch latest version")
|
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
|
const needsUpdate = currentVersion !== latestVersion
|
||||||
log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`)
|
log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`)
|
||||||
|
|
||||||
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false }
|
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { checkForUpdate } from "./checker"
|
import { checkForUpdate, getCachedVersion } from "./checker"
|
||||||
import { invalidateCache } from "./cache"
|
import { invalidatePackage } from "./cache"
|
||||||
import { PACKAGE_NAME } from "./constants"
|
import { PACKAGE_NAME } from "./constants"
|
||||||
import { log } from "../../shared/logger"
|
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
|
let hasChecked = false
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -22,21 +24,35 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput) {
|
|||||||
|
|
||||||
if (result.isLocalDev) {
|
if (result.isLocalDev) {
|
||||||
log("[auto-update-checker] Skipped: local development mode")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.needsUpdate) {
|
if (!result.needsUpdate) {
|
||||||
log("[auto-update-checker] No update needed")
|
log("[auto-update-checker] No update needed")
|
||||||
|
if (showStartupToast) {
|
||||||
|
await showVersionToast(ctx, result.currentVersion)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateCache()
|
invalidatePackage(PACKAGE_NAME)
|
||||||
|
|
||||||
await ctx.client.tui
|
await ctx.client.tui
|
||||||
.showToast({
|
.showToast({
|
||||||
body: {
|
body: {
|
||||||
title: `${PACKAGE_NAME} Update`,
|
title: `OhMyOpenCode ${result.latestVersion}`,
|
||||||
message: `v${result.latestVersion} available (current: v${result.currentVersion}). Restart OpenCode to apply.`,
|
message: `OpenCode is now on Steroids. oMoMoMoMo...\nv${result.latestVersion} available. Restart OpenCode to apply.`,
|
||||||
variant: "info" as const,
|
variant: "info" as const,
|
||||||
duration: 8000,
|
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<void> {
|
||||||
|
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 { checkForUpdate } from "./checker"
|
||||||
export { invalidateCache } from "./cache"
|
export { invalidatePackage, invalidateCache } from "./cache"
|
||||||
|
|||||||
@@ -19,4 +19,9 @@ export interface UpdateCheckResult {
|
|||||||
currentVersion: string | null
|
currentVersion: string | null
|
||||||
latestVersion: string | null
|
latestVersion: string | null
|
||||||
isLocalDev: boolean
|
isLocalDev: boolean
|
||||||
|
isPinned: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoUpdateCheckerOptions {
|
||||||
|
showStartupToast?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/index.ts
22
src/index.ts
@@ -1,5 +1,5 @@
|
|||||||
import type { Plugin } from "@opencode-ai/plugin";
|
import type { Plugin } from "@opencode-ai/plugin";
|
||||||
import { createBuiltinAgents, BUILD_AGENT_PROMPT_EXTENSION } from "./agents";
|
import { createBuiltinAgents } from "./agents";
|
||||||
import {
|
import {
|
||||||
createTodoContinuationEnforcer,
|
createTodoContinuationEnforcer,
|
||||||
createContextWindowMonitorHook,
|
createContextWindowMonitorHook,
|
||||||
@@ -203,7 +203,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
? createRulesInjectorHook(ctx)
|
? createRulesInjectorHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
||||||
? createAutoUpdateCheckerHook(ctx)
|
? createAutoUpdateCheckerHook(ctx, {
|
||||||
|
showStartupToast: isHookEnabled("startup-toast"),
|
||||||
|
})
|
||||||
: null;
|
: null;
|
||||||
const keywordDetector = isHookEnabled("keyword-detector")
|
const keywordDetector = isHookEnabled("keyword-detector")
|
||||||
? createKeywordDetectorHook()
|
? createKeywordDetectorHook()
|
||||||
@@ -252,26 +254,16 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const userAgents = (pluginConfig.claude_code?.agents ?? true) ? loadUserAgents() : {};
|
const userAgents = (pluginConfig.claude_code?.agents ?? true) ? loadUserAgents() : {};
|
||||||
const projectAgents = (pluginConfig.claude_code?.agents ?? true) ? loadProjectAgents() : {};
|
const projectAgents = (pluginConfig.claude_code?.agents ?? true) ? loadProjectAgents() : {};
|
||||||
|
|
||||||
|
const shouldHideBuild = pluginConfig.omo_agent?.disable_build !== false;
|
||||||
|
|
||||||
config.agent = {
|
config.agent = {
|
||||||
...builtinAgents,
|
...builtinAgents,
|
||||||
...userAgents,
|
...userAgents,
|
||||||
...projectAgents,
|
...projectAgents,
|
||||||
...config.agent,
|
...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 = {
|
||||||
...config.tools,
|
...config.tools,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user