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:
@@ -24,7 +24,7 @@ import {
|
||||
type PreCompactContext,
|
||||
} from "./pre-compact"
|
||||
import { cacheToolInput, getToolInput } from "./tool-input-cache"
|
||||
import { recordToolUse, recordToolResult, getTranscriptPath, recordUserMessage } from "./transcript"
|
||||
import { appendTranscriptEntry, getTranscriptPath } from "./transcript"
|
||||
import type { PluginConfig } from "./types"
|
||||
import { log, isHookDisabled } from "../../shared"
|
||||
import type { ContextCollector } from "../../features/context-injector"
|
||||
@@ -92,7 +92,11 @@ export function createClaudeCodeHooksHook(
|
||||
const textParts = output.parts.filter((p) => p.type === "text" && p.text)
|
||||
const prompt = textParts.map((p) => p.text ?? "").join("\n")
|
||||
|
||||
recordUserMessage(input.sessionID, prompt)
|
||||
appendTranscriptEntry(input.sessionID, {
|
||||
type: "user",
|
||||
timestamp: new Date().toISOString(),
|
||||
content: prompt,
|
||||
})
|
||||
|
||||
const messageParts: MessagePart[] = textParts.map((p) => ({
|
||||
type: p.type as "text",
|
||||
@@ -198,7 +202,12 @@ export function createClaudeCodeHooksHook(
|
||||
const claudeConfig = await loadClaudeHooksConfig()
|
||||
const extendedConfig = await loadPluginExtendedConfig()
|
||||
|
||||
recordToolUse(input.sessionID, input.tool, output.args as Record<string, unknown>)
|
||||
appendTranscriptEntry(input.sessionID, {
|
||||
type: "tool_use",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: input.tool,
|
||||
tool_input: output.args as Record<string, unknown>,
|
||||
})
|
||||
|
||||
cacheToolInput(input.sessionID, input.tool, input.callID, output.args as Record<string, unknown>)
|
||||
|
||||
@@ -253,7 +262,13 @@ export function createClaudeCodeHooksHook(
|
||||
const metadata = output.metadata as Record<string, unknown> | undefined
|
||||
const hasMetadata = metadata && typeof metadata === "object" && Object.keys(metadata).length > 0
|
||||
const toolOutput = hasMetadata ? metadata : { output: output.output }
|
||||
recordToolResult(input.sessionID, input.tool, cachedInput, toolOutput)
|
||||
appendTranscriptEntry(input.sessionID, {
|
||||
type: "tool_result",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: input.tool,
|
||||
tool_input: cachedInput,
|
||||
tool_output: toolOutput,
|
||||
})
|
||||
|
||||
if (!isHookDisabled(config, "PostToolUse")) {
|
||||
const postClient: PostToolUseClient = {
|
||||
|
||||
@@ -28,56 +28,6 @@ export function appendTranscriptEntry(
|
||||
appendFileSync(path, line)
|
||||
}
|
||||
|
||||
export function recordToolUse(
|
||||
sessionId: string,
|
||||
toolName: string,
|
||||
toolInput: Record<string, unknown>
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "tool_use",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
})
|
||||
}
|
||||
|
||||
export function recordToolResult(
|
||||
sessionId: string,
|
||||
toolName: string,
|
||||
toolInput: Record<string, unknown>,
|
||||
toolOutput: Record<string, unknown>
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "tool_result",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
tool_output: toolOutput,
|
||||
})
|
||||
}
|
||||
|
||||
export function recordUserMessage(
|
||||
sessionId: string,
|
||||
content: string
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "user",
|
||||
timestamp: new Date().toISOString(),
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
export function recordAssistantMessage(
|
||||
sessionId: string,
|
||||
content: string
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "assistant",
|
||||
timestamp: new Date().toISOString(),
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Claude Code Compatible Transcript Builder (PORT FROM DISABLED)
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { spawn } from "bun"
|
||||
import { existsSync, mkdirSync, chmodSync, unlinkSync, appendFileSync } from "fs"
|
||||
import { existsSync, appendFileSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { homedir, tmpdir } from "os"
|
||||
import { createRequire } from "module"
|
||||
import { extractZip } from "../../shared"
|
||||
import {
|
||||
cleanupArchive,
|
||||
downloadArchive,
|
||||
ensureCacheDir,
|
||||
ensureExecutable,
|
||||
extractTarGz,
|
||||
extractZipArchive,
|
||||
getCachedBinaryPath as getCachedBinaryPathShared,
|
||||
} from "../../shared/binary-downloader"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
|
||||
@@ -60,8 +67,7 @@ export function getBinaryName(): string {
|
||||
* Get the cached binary path if it exists.
|
||||
*/
|
||||
export function getCachedBinaryPath(): string | null {
|
||||
const binaryPath = join(getCacheDir(), getBinaryName())
|
||||
return existsSync(binaryPath) ? binaryPath : null
|
||||
return getCachedBinaryPathShared(getCacheDir(), getBinaryName())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,27 +84,6 @@ function getPackageVersion(): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tar.gz archive using system tar command.
|
||||
*/
|
||||
async function extractTarGz(archivePath: string, destDir: string): Promise<void> {
|
||||
debugLog("Extracting tar.gz:", archivePath, "to", destDir)
|
||||
|
||||
const proc = spawn(["tar", "-xzf", archivePath, "-C", destDir], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const exitCode = await proc.exited
|
||||
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
throw new Error(`tar extraction failed (exit ${exitCode}): ${stderr}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Download the comment-checker binary from GitHub Releases.
|
||||
* Returns the path to the downloaded binary, or null on failure.
|
||||
@@ -132,39 +117,26 @@ export async function downloadCommentChecker(): Promise<string | null> {
|
||||
|
||||
try {
|
||||
// Ensure cache directory exists
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Download with fetch() - Bun handles redirects automatically
|
||||
const response = await fetch(downloadUrl, { redirect: "follow" })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
ensureCacheDir(cacheDir)
|
||||
|
||||
const archivePath = join(cacheDir, assetName)
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
await Bun.write(archivePath, arrayBuffer)
|
||||
await downloadArchive(downloadUrl, archivePath)
|
||||
|
||||
debugLog(`Downloaded archive to: ${archivePath}`)
|
||||
|
||||
// Extract based on file type
|
||||
if (ext === "tar.gz") {
|
||||
debugLog("Extracting tar.gz:", archivePath, "to", cacheDir)
|
||||
await extractTarGz(archivePath, cacheDir)
|
||||
} else {
|
||||
await extractZip(archivePath, cacheDir)
|
||||
await extractZipArchive(archivePath, cacheDir)
|
||||
}
|
||||
|
||||
// Clean up archive
|
||||
if (existsSync(archivePath)) {
|
||||
unlinkSync(archivePath)
|
||||
}
|
||||
cleanupArchive(archivePath)
|
||||
|
||||
// Set execute permission on Unix
|
||||
if (process.platform !== "win32" && existsSync(binaryPath)) {
|
||||
chmodSync(binaryPath, 0o755)
|
||||
}
|
||||
ensureExecutable(binaryPath)
|
||||
|
||||
debugLog(`Successfully downloaded binary to: ${binaryPath}`)
|
||||
log(`[oh-my-opencode] comment-checker binary ready.`)
|
||||
|
||||
@@ -1,48 +1,8 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { AGENTS_INJECTOR_STORAGE } from "./constants";
|
||||
import type { InjectedPathsData } from "./types";
|
||||
import { createInjectedPathsStorage } from "../../shared/session-injected-paths";
|
||||
|
||||
function getStoragePath(sessionID: string): string {
|
||||
return join(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
|
||||
}
|
||||
|
||||
export function loadInjectedPaths(sessionID: string): Set<string> {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return new Set();
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const data: InjectedPathsData = JSON.parse(content);
|
||||
return new Set(data.injectedPaths);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveInjectedPaths(sessionID: string, paths: Set<string>): void {
|
||||
if (!existsSync(AGENTS_INJECTOR_STORAGE)) {
|
||||
mkdirSync(AGENTS_INJECTOR_STORAGE, { recursive: true });
|
||||
}
|
||||
|
||||
const data: InjectedPathsData = {
|
||||
sessionID,
|
||||
injectedPaths: [...paths],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeFileSync(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function clearInjectedPaths(sessionID: string): void {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
export const {
|
||||
loadInjectedPaths,
|
||||
saveInjectedPaths,
|
||||
clearInjectedPaths,
|
||||
} = createInjectedPathsStorage(AGENTS_INJECTOR_STORAGE);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface InjectedPathsData {
|
||||
sessionID: string;
|
||||
injectedPaths: string[];
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -1,48 +1,8 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { README_INJECTOR_STORAGE } from "./constants";
|
||||
import type { InjectedPathsData } from "./types";
|
||||
import { createInjectedPathsStorage } from "../../shared/session-injected-paths";
|
||||
|
||||
function getStoragePath(sessionID: string): string {
|
||||
return join(README_INJECTOR_STORAGE, `${sessionID}.json`);
|
||||
}
|
||||
|
||||
export function loadInjectedPaths(sessionID: string): Set<string> {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return new Set();
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const data: InjectedPathsData = JSON.parse(content);
|
||||
return new Set(data.injectedPaths);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveInjectedPaths(sessionID: string, paths: Set<string>): void {
|
||||
if (!existsSync(README_INJECTOR_STORAGE)) {
|
||||
mkdirSync(README_INJECTOR_STORAGE, { recursive: true });
|
||||
}
|
||||
|
||||
const data: InjectedPathsData = {
|
||||
sessionID,
|
||||
injectedPaths: [...paths],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeFileSync(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function clearInjectedPaths(sessionID: string): void {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
export const {
|
||||
loadInjectedPaths,
|
||||
saveInjectedPaths,
|
||||
clearInjectedPaths,
|
||||
} = createInjectedPathsStorage(README_INJECTOR_STORAGE);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface InjectedPathsData {
|
||||
sessionID: string;
|
||||
injectedPaths: string[];
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -2,24 +2,6 @@ import { spawn } from "bun"
|
||||
|
||||
type Platform = "darwin" | "linux" | "win32" | "unsupported"
|
||||
|
||||
let notifySendPath: string | null = null
|
||||
let notifySendPromise: Promise<string | null> | null = null
|
||||
|
||||
let osascriptPath: string | null = null
|
||||
let osascriptPromise: Promise<string | null> | null = null
|
||||
|
||||
let powershellPath: string | null = null
|
||||
let powershellPromise: Promise<string | null> | null = null
|
||||
|
||||
let afplayPath: string | null = null
|
||||
let afplayPromise: Promise<string | null> | null = null
|
||||
|
||||
let paplayPath: string | null = null
|
||||
let paplayPromise: Promise<string | null> | null = null
|
||||
|
||||
let aplayPath: string | null = null
|
||||
let aplayPromise: Promise<string | null> | null = null
|
||||
|
||||
async function findCommand(commandName: string): Promise<string | null> {
|
||||
const isWindows = process.platform === "win32"
|
||||
const cmd = isWindows ? "where" : "which"
|
||||
@@ -48,83 +30,30 @@ async function findCommand(commandName: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNotifySendPath(): Promise<string | null> {
|
||||
if (notifySendPath !== null) return notifySendPath
|
||||
if (notifySendPromise) return notifySendPromise
|
||||
function createCommandFinder(commandName: string): () => Promise<string | null> {
|
||||
let cachedPath: string | null = null
|
||||
let pending: Promise<string | null> | null = null
|
||||
|
||||
notifySendPromise = (async () => {
|
||||
const path = await findCommand("notify-send")
|
||||
notifySendPath = path
|
||||
return path
|
||||
})()
|
||||
return async () => {
|
||||
if (cachedPath !== null) return cachedPath
|
||||
if (pending) return pending
|
||||
|
||||
return notifySendPromise
|
||||
pending = (async () => {
|
||||
const path = await findCommand(commandName)
|
||||
cachedPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOsascriptPath(): Promise<string | null> {
|
||||
if (osascriptPath !== null) return osascriptPath
|
||||
if (osascriptPromise) return osascriptPromise
|
||||
|
||||
osascriptPromise = (async () => {
|
||||
const path = await findCommand("osascript")
|
||||
osascriptPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return osascriptPromise
|
||||
}
|
||||
|
||||
export async function getPowershellPath(): Promise<string | null> {
|
||||
if (powershellPath !== null) return powershellPath
|
||||
if (powershellPromise) return powershellPromise
|
||||
|
||||
powershellPromise = (async () => {
|
||||
const path = await findCommand("powershell")
|
||||
powershellPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return powershellPromise
|
||||
}
|
||||
|
||||
export async function getAfplayPath(): Promise<string | null> {
|
||||
if (afplayPath !== null) return afplayPath
|
||||
if (afplayPromise) return afplayPromise
|
||||
|
||||
afplayPromise = (async () => {
|
||||
const path = await findCommand("afplay")
|
||||
afplayPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return afplayPromise
|
||||
}
|
||||
|
||||
export async function getPaplayPath(): Promise<string | null> {
|
||||
if (paplayPath !== null) return paplayPath
|
||||
if (paplayPromise) return paplayPromise
|
||||
|
||||
paplayPromise = (async () => {
|
||||
const path = await findCommand("paplay")
|
||||
paplayPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return paplayPromise
|
||||
}
|
||||
|
||||
export async function getAplayPath(): Promise<string | null> {
|
||||
if (aplayPath !== null) return aplayPath
|
||||
if (aplayPromise) return aplayPromise
|
||||
|
||||
aplayPromise = (async () => {
|
||||
const path = await findCommand("aplay")
|
||||
aplayPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return aplayPromise
|
||||
}
|
||||
export const getNotifySendPath = createCommandFinder("notify-send")
|
||||
export const getOsascriptPath = createCommandFinder("osascript")
|
||||
export const getPowershellPath = createCommandFinder("powershell")
|
||||
export const getAfplayPath = createCommandFinder("afplay")
|
||||
export const getPaplayPath = createCommandFinder("paplay")
|
||||
export const getAplayPath = createCommandFinder("aplay")
|
||||
|
||||
export function startBackgroundCheck(platform: Platform): void {
|
||||
if (platform === "darwin") {
|
||||
|
||||
Reference in New Issue
Block a user