Skip to content

Commit 35d53cc

Browse files
committed
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)
1 parent 9a1a22d commit 35d53cc

File tree

7 files changed

+125
-42
lines changed

7 files changed

+125
-42
lines changed

src/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export {
55
McpNameSchema,
66
AgentNameSchema,
77
HookNameSchema,
8+
OmoAgentConfigSchema,
89
} from "./schema"
910

1011
export type {
@@ -14,4 +15,5 @@ export type {
1415
McpName,
1516
AgentName,
1617
HookName,
18+
OmoAgentConfig,
1719
} from "./schema"

src/config/schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const HookNameSchema = z.enum([
5353
"rules-injector",
5454
"background-notification",
5555
"auto-update-checker",
56+
"startup-toast",
5657
"keyword-detector",
5758
"agent-usage-reminder",
5859
])
@@ -93,6 +94,10 @@ export const ClaudeCodeConfigSchema = z.object({
9394
hooks: z.boolean().optional(),
9495
})
9596

97+
export const OmoAgentConfigSchema = z.object({
98+
disable_build: z.boolean().optional(),
99+
})
100+
96101
export const OhMyOpenCodeConfigSchema = z.object({
97102
$schema: z.string().optional(),
98103
disabled_mcps: z.array(McpNameSchema).optional(),
@@ -101,12 +106,14 @@ export const OhMyOpenCodeConfigSchema = z.object({
101106
agents: AgentOverridesSchema.optional(),
102107
claude_code: ClaudeCodeConfigSchema.optional(),
103108
google_auth: z.boolean().optional(),
109+
omo_agent: OmoAgentConfigSchema.optional(),
104110
})
105111

106112
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
107113
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
108114
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
109115
export type AgentName = z.infer<typeof AgentNameSchema>
110116
export type HookName = z.infer<typeof HookNameSchema>
117+
export type OmoAgentConfig = z.infer<typeof OmoAgentConfigSchema>
111118

112119
export { McpNameSchema, type McpName } from "../mcp/types"
Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,47 @@
11
import * as fs from "node:fs"
2-
import { VERSION_FILE } from "./constants"
2+
import * as path from "node:path"
3+
import { CACHE_DIR, PACKAGE_NAME } from "./constants"
34
import { log } from "../../shared/logger"
45

5-
export function invalidateCache(): boolean {
6+
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
67
try {
7-
if (fs.existsSync(VERSION_FILE)) {
8-
fs.unlinkSync(VERSION_FILE)
9-
log(`[auto-update-checker] Cache invalidated: ${VERSION_FILE}`)
10-
return true
8+
const pkgDir = path.join(CACHE_DIR, "node_modules", packageName)
9+
const pkgJsonPath = path.join(CACHE_DIR, "package.json")
10+
11+
let packageRemoved = false
12+
let dependencyRemoved = false
13+
14+
if (fs.existsSync(pkgDir)) {
15+
fs.rmSync(pkgDir, { recursive: true, force: true })
16+
log(`[auto-update-checker] Package removed: ${pkgDir}`)
17+
packageRemoved = true
1118
}
12-
log("[auto-update-checker] Version file not found, nothing to invalidate")
13-
return false
19+
20+
if (fs.existsSync(pkgJsonPath)) {
21+
const content = fs.readFileSync(pkgJsonPath, "utf-8")
22+
const pkgJson = JSON.parse(content)
23+
if (pkgJson.dependencies?.[packageName]) {
24+
delete pkgJson.dependencies[packageName]
25+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2))
26+
log(`[auto-update-checker] Dependency removed from package.json: ${packageName}`)
27+
dependencyRemoved = true
28+
}
29+
}
30+
31+
if (!packageRemoved && !dependencyRemoved) {
32+
log(`[auto-update-checker] Package not found, nothing to invalidate: ${packageName}`)
33+
return false
34+
}
35+
36+
return true
1437
} catch (err) {
15-
log("[auto-update-checker] Failed to invalidate cache:", err)
38+
log("[auto-update-checker] Failed to invalidate package:", err)
1639
return false
1740
}
1841
}
42+
43+
/** @deprecated Use invalidatePackage instead - this nukes ALL plugins */
44+
export function invalidateCache(): boolean {
45+
log("[auto-update-checker] WARNING: invalidateCache is deprecated, use invalidatePackage")
46+
return invalidatePackage()
47+
}

src/hooks/auto-update-checker/checker.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ export function isLocalDevMode(directory: string): boolean {
3333
return false
3434
}
3535

36-
export function findPluginEntry(directory: string): string | null {
36+
export interface PluginEntryInfo {
37+
entry: string
38+
isPinned: boolean
39+
pinnedVersion: string | null
40+
}
41+
42+
export function findPluginEntry(directory: string): PluginEntryInfo | null {
3743
const projectConfig = path.join(directory, ".opencode", "opencode.json")
3844

3945
for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) {
@@ -44,8 +50,13 @@ export function findPluginEntry(directory: string): string | null {
4450
const plugins = config.plugin ?? []
4551

4652
for (const entry of plugins) {
47-
if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`)) {
48-
return entry
53+
if (entry === PACKAGE_NAME) {
54+
return { entry, isPinned: false, pinnedVersion: null }
55+
}
56+
if (entry.startsWith(`${PACKAGE_NAME}@`)) {
57+
const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1)
58+
const isPinned = pinnedVersion !== "latest"
59+
return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null }
4960
}
5061
}
5162
} catch {
@@ -91,29 +102,35 @@ export async function getLatestVersion(): Promise<string | null> {
91102
export async function checkForUpdate(directory: string): Promise<UpdateCheckResult> {
92103
if (isLocalDevMode(directory)) {
93104
log("[auto-update-checker] Local dev mode detected, skipping update check")
94-
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true }
105+
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true, isPinned: false }
95106
}
96107

97-
const pluginEntry = findPluginEntry(directory)
98-
if (!pluginEntry) {
108+
const pluginInfo = findPluginEntry(directory)
109+
if (!pluginInfo) {
99110
log("[auto-update-checker] Plugin not found in config")
100-
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false }
111+
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false }
112+
}
113+
114+
// Respect version pinning
115+
if (pluginInfo.isPinned) {
116+
log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`)
117+
return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true }
101118
}
102119

103120
const currentVersion = getCachedVersion()
104121
if (!currentVersion) {
105122
log("[auto-update-checker] No cached version found")
106-
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false }
123+
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false }
107124
}
108125

109126
const latestVersion = await getLatestVersion()
110127
if (!latestVersion) {
111128
log("[auto-update-checker] Failed to fetch latest version")
112-
return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false }
129+
return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false }
113130
}
114131

115132
const needsUpdate = currentVersion !== latestVersion
116133
log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`)
117134

118-
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false }
135+
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false }
119136
}

src/hooks/auto-update-checker/index.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { PluginInput } from "@opencode-ai/plugin"
2-
import { checkForUpdate } from "./checker"
3-
import { invalidateCache } from "./cache"
2+
import { checkForUpdate, getCachedVersion } from "./checker"
3+
import { invalidatePackage } from "./cache"
44
import { PACKAGE_NAME } from "./constants"
55
import { log } from "../../shared/logger"
6+
import type { AutoUpdateCheckerOptions } from "./types"
67

7-
export function createAutoUpdateCheckerHook(ctx: PluginInput) {
8+
export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) {
9+
const { showStartupToast = true } = options
810
let hasChecked = false
911

1012
return {
@@ -22,21 +24,35 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput) {
2224

2325
if (result.isLocalDev) {
2426
log("[auto-update-checker] Skipped: local development mode")
27+
if (showStartupToast) {
28+
await showVersionToast(ctx, getCachedVersion())
29+
}
30+
return
31+
}
32+
33+
if (result.isPinned) {
34+
log(`[auto-update-checker] Skipped: version pinned to ${result.currentVersion}`)
35+
if (showStartupToast) {
36+
await showVersionToast(ctx, result.currentVersion)
37+
}
2538
return
2639
}
2740

2841
if (!result.needsUpdate) {
2942
log("[auto-update-checker] No update needed")
43+
if (showStartupToast) {
44+
await showVersionToast(ctx, result.currentVersion)
45+
}
3046
return
3147
}
3248

33-
invalidateCache()
49+
invalidatePackage(PACKAGE_NAME)
3450

3551
await ctx.client.tui
3652
.showToast({
3753
body: {
38-
title: `${PACKAGE_NAME} Update`,
39-
message: `v${result.latestVersion} available (current: v${result.currentVersion}). Restart OpenCode to apply.`,
54+
title: `OhMyOpenCode ${result.latestVersion}`,
55+
message: `OpenCode is now on Steroids. oMoMoMoMo...\nv${result.latestVersion} available. Restart OpenCode to apply.`,
4056
variant: "info" as const,
4157
duration: 8000,
4258
},
@@ -51,6 +67,21 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput) {
5167
}
5268
}
5369

54-
export type { UpdateCheckResult } from "./types"
70+
async function showVersionToast(ctx: PluginInput, version: string | null): Promise<void> {
71+
const displayVersion = version ?? "unknown"
72+
await ctx.client.tui
73+
.showToast({
74+
body: {
75+
title: `OhMyOpenCode ${displayVersion}`,
76+
message: "OpenCode is now on Steroids. oMoMoMoMo...",
77+
variant: "info" as const,
78+
duration: 5000,
79+
},
80+
})
81+
.catch(() => {})
82+
log(`[auto-update-checker] Startup toast shown: v${displayVersion}`)
83+
}
84+
85+
export type { UpdateCheckResult, AutoUpdateCheckerOptions } from "./types"
5586
export { checkForUpdate } from "./checker"
56-
export { invalidateCache } from "./cache"
87+
export { invalidatePackage, invalidateCache } from "./cache"

src/hooks/auto-update-checker/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ export interface UpdateCheckResult {
1919
currentVersion: string | null
2020
latestVersion: string | null
2121
isLocalDev: boolean
22+
isPinned: boolean
23+
}
24+
25+
export interface AutoUpdateCheckerOptions {
26+
showStartupToast?: boolean
2227
}

src/index.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Plugin } from "@opencode-ai/plugin";
2-
import { createBuiltinAgents, BUILD_AGENT_PROMPT_EXTENSION } from "./agents";
2+
import { createBuiltinAgents } from "./agents";
33
import {
44
createTodoContinuationEnforcer,
55
createContextWindowMonitorHook,
@@ -203,7 +203,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
203203
? createRulesInjectorHook(ctx)
204204
: null;
205205
const autoUpdateChecker = isHookEnabled("auto-update-checker")
206-
? createAutoUpdateCheckerHook(ctx)
206+
? createAutoUpdateCheckerHook(ctx, {
207+
showStartupToast: isHookEnabled("startup-toast"),
208+
})
207209
: null;
208210
const keywordDetector = isHookEnabled("keyword-detector")
209211
? createKeywordDetectorHook()
@@ -252,26 +254,16 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
252254
const userAgents = (pluginConfig.claude_code?.agents ?? true) ? loadUserAgents() : {};
253255
const projectAgents = (pluginConfig.claude_code?.agents ?? true) ? loadProjectAgents() : {};
254256

257+
const shouldHideBuild = pluginConfig.omo_agent?.disable_build !== false;
258+
255259
config.agent = {
256260
...builtinAgents,
257261
...userAgents,
258262
...projectAgents,
259263
...config.agent,
264+
...(shouldHideBuild ? { build: { mode: "subagent" } } : {}),
260265
};
261266

262-
// Inject orchestration prompt to all non-subagent agents
263-
// Subagents are delegated TO, so they don't need orchestration guidance
264-
for (const [agentName, agentConfig] of Object.entries(config.agent ?? {})) {
265-
if (agentConfig && agentConfig.mode !== "subagent") {
266-
const existingPrompt = agentConfig.prompt || "";
267-
const userOverride = pluginConfig.agents?.[agentName as keyof typeof pluginConfig.agents]?.prompt || "";
268-
config.agent[agentName] = {
269-
...agentConfig,
270-
prompt: existingPrompt + BUILD_AGENT_PROMPT_EXTENSION + userOverride,
271-
};
272-
}
273-
}
274-
275267
config.tools = {
276268
...config.tools,
277269
};

0 commit comments

Comments
 (0)