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:
@@ -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,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.`
|
||||
}
|
||||
|
||||
|
||||
@@ -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.`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user