Consolidate duplicate patterns and simplify codebase (#1317)

* refactor(shared): unify binary downloader and session path storage

- Create binary-downloader.ts for common download/extract logic
- Create session-injected-paths.ts for unified path tracking
- Refactor comment-checker, ast-grep, grep downloaders to use shared util
- Consolidate directory injector types into shared module

* feat(shared): implement unified model resolution pipeline

- Create ModelResolutionPipeline for centralized model selection
- Refactor model-resolver to use pipeline
- Update delegate-task and config-handler to use unified logic
- Ensure consistent model resolution across all agent types

* refactor(agents): simplify agent utils and metadata management

- Extract helper functions for config merging and env context
- Register prompt metadata for all agents
- Simplify agent variant detection logic

* cleanup: inline utilities and remove unused exports

- Remove case-insensitive.ts (inline with native JS)
- Simplify opencode-version helpers
- Remove unused getModelLimit, createCompactionContextInjector exports
- Inline transcript entry creation in claude-code-hooks
- Update tests accordingly

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
YeonGyu-Kim
2026-01-31 15:46:14 +09:00
committed by GitHub
parent 4b5e38f8f8
commit 4a82ff40fb
31 changed files with 597 additions and 848 deletions
+15 -26
View File
@@ -1,8 +1,15 @@
import { existsSync, mkdirSync, chmodSync, unlinkSync } from "fs"
import { existsSync } from "fs"
import { join } from "path"
import { homedir } from "os"
import { createRequire } from "module"
import { extractZip } from "../../shared"
import {
cleanupArchive,
downloadArchive,
ensureCacheDir,
ensureExecutable,
extractZipArchive,
getCachedBinaryPath as getCachedBinaryPathShared,
} from "../../shared/binary-downloader"
import { log } from "../../shared/logger"
const REPO = "ast-grep/ast-grep"
@@ -53,8 +60,7 @@ export function getBinaryName(): string {
}
export function getCachedBinaryPath(): string | null {
const binaryPath = join(getCacheDir(), getBinaryName())
return existsSync(binaryPath) ? binaryPath : null
return getCachedBinaryPathShared(getCacheDir(), getBinaryName())
}
@@ -83,29 +89,12 @@ export async function downloadAstGrep(version: string = DEFAULT_VERSION): Promis
log(`[oh-my-opencode] Downloading ast-grep binary...`)
try {
if (!existsSync(cacheDir)) {
mkdirSync(cacheDir, { recursive: true })
}
const response = await fetch(downloadUrl, { redirect: "follow" })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const archivePath = join(cacheDir, assetName)
const arrayBuffer = await response.arrayBuffer()
await Bun.write(archivePath, arrayBuffer)
await extractZip(archivePath, cacheDir)
if (existsSync(archivePath)) {
unlinkSync(archivePath)
}
if (process.platform !== "win32" && existsSync(binaryPath)) {
chmodSync(binaryPath, 0o755)
}
ensureCacheDir(cacheDir)
await downloadArchive(downloadUrl, archivePath)
await extractZipArchive(archivePath, cacheDir)
cleanupArchive(archivePath)
ensureExecutable(binaryPath)
log(`[oh-my-opencode] ast-grep binary ready.`)
+4 -2
View File
@@ -4,7 +4,7 @@ import { join } from "node:path"
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
import type { CallOmoAgentArgs } from "./types"
import type { BackgroundManager } from "../../features/background-agent"
import { log, getAgentToolRestrictions, includesCaseInsensitive } from "../../shared"
import { log, getAgentToolRestrictions } from "../../shared"
import { consumeNewMessages } from "../../shared/session-cursor"
import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
import { getSessionAgent } from "../../features/claude-code-session-state"
@@ -58,7 +58,9 @@ export function createCallOmoAgent(
log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`)
// Case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc.
if (!includesCaseInsensitive([...ALLOWED_AGENTS], args.subagent_type)) {
if (![...ALLOWED_AGENTS].some(
(name) => name.toLowerCase() === args.subagent_type.toLowerCase()
)) {
return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`
}
+20 -15
View File
@@ -12,10 +12,9 @@ import { discoverSkills } from "../../features/opencode-skill-loader"
import { getTaskToastManager } from "../../features/task-toast-manager"
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
import { log, getAgentToolRestrictions, resolveModel, getOpenCodeConfigPaths, findByNameCaseInsensitive, equalsIgnoreCase, promptWithModelSuggestionRetry } from "../../shared"
import { log, getAgentToolRestrictions, resolveModel, resolveModelPipeline, getOpenCodeConfigPaths, promptWithModelSuggestionRetry } from "../../shared"
import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability"
import { readConnectedProvidersCache } from "../../shared/connected-providers-cache"
import { resolveModelWithFallback } from "../../shared/model-resolver"
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
type OpencodeClient = PluginInput["client"]
@@ -552,16 +551,20 @@ To continue this session: session_id="${args.session_id}"`
modelInfo = { model: actualModel, type: "system-default", source: "system-default" }
}
} else {
const resolution = resolveModelWithFallback({
const resolution = resolveModelPipeline({
intent: {
userModel: userCategories?.[args.category]?.model,
categoryDefaultModel: resolved.model ?? sisyphusJuniorModel,
},
constraints: { availableModels },
policy: {
fallbackChain: requirement.fallbackChain,
availableModels,
systemDefaultModel,
})
},
})
if (resolution) {
const { model: resolvedModel, source, variant: resolvedVariant } = resolution
if (resolution) {
const { model: resolvedModel, provenance, variant: resolvedVariant } = resolution
actualModel = resolvedModel
if (!parseModelString(actualModel)) {
@@ -569,7 +572,8 @@ To continue this session: session_id="${args.session_id}"`
}
let type: "user-defined" | "inherited" | "category-default" | "system-default"
switch (source) {
const source = provenance
switch (provenance) {
case "override":
type = "user-defined"
break
@@ -582,7 +586,7 @@ To continue this session: session_id="${args.session_id}"`
break
}
modelInfo = { model: actualModel, type, source }
modelInfo = { model: actualModel, type, source }
const parsedModel = parseModelString(actualModel)
const variantToUse = userCategories?.[args.category]?.variant ?? resolvedVariant ?? resolved.config.variant
@@ -780,7 +784,7 @@ To continue this session: session_id="${sessionID}"`
}
const agentName = args.subagent_type.trim()
if (equalsIgnoreCase(agentName, SISYPHUS_JUNIOR_AGENT)) {
if (agentName.toLowerCase() === SISYPHUS_JUNIOR_AGENT.toLowerCase()) {
return `Cannot use subagent_type="${SISYPHUS_JUNIOR_AGENT}" directly. Use category parameter instead (e.g., ${categoryExamples}).
Sisyphus-Junior is spawned automatically when you specify a category. Pick the appropriate category for your task domain.`
@@ -803,12 +807,13 @@ Create the work plan directly - that's your job as the planning agent.`
const callableAgents = agents.filter((a) => a.mode !== "primary")
const matchedAgent = findByNameCaseInsensitive(callableAgents, agentToUse)
const matchedAgent = callableAgents.find(
(agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()
)
if (!matchedAgent) {
const isPrimaryAgent = findByNameCaseInsensitive(
agents.filter((a) => a.mode === "primary"),
agentToUse
)
const isPrimaryAgent = agents
.filter((a) => a.mode === "primary")
.find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase())
if (isPrimaryAgent) {
return `Cannot call primary agent "${isPrimaryAgent.name}" via delegate_task. Primary agents are top-level orchestrators.`
}
+16 -34
View File
@@ -1,7 +1,13 @@
import { existsSync, mkdirSync, chmodSync, unlinkSync, readdirSync } from "node:fs"
import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path"
import { spawn } from "bun"
import { extractZip as extractZipBase } from "../../shared"
import {
cleanupArchive,
downloadArchive,
ensureCacheDir,
ensureExecutable,
extractTarGz as extractTarGzArchive,
} from "../../shared/binary-downloader"
export function findFileRecursive(dir: string, filename: string): string | null {
try {
@@ -41,16 +47,6 @@ function getRgPath(): string {
return join(getInstallDir(), isWindows ? "rg.exe" : "rg")
}
async function downloadFile(url: string, destPath: string): Promise<void> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to download: ${response.status} ${response.statusText}`)
}
const buffer = await response.arrayBuffer()
await Bun.write(destPath, buffer)
}
async function extractTarGz(archivePath: string, destDir: string): Promise<void> {
const platformKey = getPlatformKey()
@@ -62,17 +58,7 @@ async function extractTarGz(archivePath: string, destDir: string): Promise<void>
args.push("--wildcards", "*/rg")
}
const proc = spawn(args, {
cwd: destDir,
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
if (exitCode !== 0) {
const stderr = await new Response(proc.stderr).text()
throw new Error(`Failed to extract tar.gz: ${stderr}`)
}
await extractTarGzArchive(archivePath, destDir, { args, cwd: destDir })
}
async function extractZip(archivePath: string, destDir: string): Promise<void> {
@@ -104,14 +90,14 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
return rgPath
}
mkdirSync(installDir, { recursive: true })
ensureCacheDir(installDir)
const filename = `ripgrep-${RG_VERSION}-${config.platform}.${config.extension}`
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`
const archivePath = join(installDir, filename)
try {
await downloadFile(url, archivePath)
await downloadArchive(url, archivePath)
if (config.extension === "tar.gz") {
await extractTarGz(archivePath, installDir)
@@ -119,9 +105,7 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
await extractZip(archivePath, installDir)
}
if (process.platform !== "win32") {
chmodSync(rgPath, 0o755)
}
ensureExecutable(rgPath)
if (!existsSync(rgPath)) {
throw new Error("ripgrep binary not found after extraction")
@@ -129,12 +113,10 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
return rgPath
} finally {
if (existsSync(archivePath)) {
try {
unlinkSync(archivePath)
} catch {
// Cleanup failures are non-critical
}
try {
cleanupArchive(archivePath)
} catch {
// Cleanup failures are non-critical
}
}
}
+4 -2
View File
@@ -3,7 +3,7 @@ import { pathToFileURL } from "node:url"
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
import { LOOK_AT_DESCRIPTION, MULTIMODAL_LOOKER_AGENT } from "./constants"
import type { LookAtArgs } from "./types"
import { findByNameCaseInsensitive, log, promptWithModelSuggestionRetry } from "../../shared"
import { log, promptWithModelSuggestionRetry } from "../../shared"
interface LookAtArgsWithAlias extends LookAtArgs {
path?: string
@@ -143,7 +143,9 @@ Original error: ${createResult.error}`
}
const agents = ((agentsResult as { data?: AgentInfo[] })?.data ?? agentsResult) as AgentInfo[] | undefined
if (agents?.length) {
const matchedAgent = findByNameCaseInsensitive(agents, MULTIMODAL_LOOKER_AGENT)
const matchedAgent = agents.find(
(agent) => agent.name.toLowerCase() === MULTIMODAL_LOOKER_AGENT.toLowerCase()
)
if (matchedAgent?.model) {
agentModel = matchedAgent.model
}