Files
oh-my-opencode/src/hooks/hashline-read-enhancer/hook.ts
T

132 lines
3.6 KiB
TypeScript

import type { PluginInput } from "@opencode-ai/plugin"
import { computeLineHash } from "../../tools/hashline-edit/hash-computation"
import { toHashlineContent } from "../../tools/hashline-edit/diff-utils"
interface HashlineReadEnhancerConfig {
hashline_edit?: { enabled: boolean }
}
const READ_LINE_PATTERN = /^(\d+): (.*)$/
const CONTENT_OPEN_TAG = "<content>"
const CONTENT_CLOSE_TAG = "</content>"
function isReadTool(toolName: string): boolean {
return toolName.toLowerCase() === "read"
}
function isWriteTool(toolName: string): boolean {
return toolName.toLowerCase() === "write"
}
function shouldProcess(config: HashlineReadEnhancerConfig): boolean {
return config.hashline_edit?.enabled ?? false
}
function isTextFile(output: string): boolean {
const firstLine = output.split("\n")[0] ?? ""
return READ_LINE_PATTERN.test(firstLine)
}
function transformLine(line: string): string {
const match = READ_LINE_PATTERN.exec(line)
if (!match) {
return line
}
const lineNumber = parseInt(match[1], 10)
const content = match[2]
const hash = computeLineHash(lineNumber, content)
return `${lineNumber}#${hash}:${content}`
}
function transformOutput(output: string): string {
if (!output) {
return output
}
const lines = output.split("\n")
const contentStart = lines.indexOf(CONTENT_OPEN_TAG)
const contentEnd = lines.indexOf(CONTENT_CLOSE_TAG)
if (contentStart === -1 || contentEnd === -1 || contentEnd <= contentStart + 1) {
return output
}
const fileLines = lines.slice(contentStart + 1, contentEnd)
if (!isTextFile(fileLines[0] ?? "")) {
return output
}
const result: string[] = []
for (const line of fileLines) {
if (!READ_LINE_PATTERN.test(line)) {
result.push(...fileLines.slice(result.length))
break
}
result.push(transformLine(line))
}
return [...lines.slice(0, contentStart + 1), ...result, ...lines.slice(contentEnd)].join("\n")
}
function extractFilePath(metadata: unknown): string | undefined {
if (!metadata || typeof metadata !== "object") {
return undefined
}
const objectMeta = metadata as Record<string, unknown>
const candidates = [objectMeta.filepath, objectMeta.filePath, objectMeta.path, objectMeta.file]
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.length > 0) {
return candidate
}
}
return undefined
}
async function appendWriteHashlineOutput(output: { output: string; metadata: unknown }): Promise<void> {
if (output.output.includes("Updated file (LINE#ID:content):")) {
return
}
const filePath = extractFilePath(output.metadata)
if (!filePath) {
return
}
const file = Bun.file(filePath)
if (!(await file.exists())) {
return
}
const content = await file.text()
const hashlined = toHashlineContent(content)
output.output = `${output.output}\n\nUpdated file (LINE#ID:content):\n${hashlined}`
}
export function createHashlineReadEnhancerHook(
_ctx: PluginInput,
config: HashlineReadEnhancerConfig
) {
return {
"tool.execute.after": async (
input: { tool: string; sessionID: string; callID: string },
output: { title: string; output: string; metadata: unknown }
) => {
if (!isReadTool(input.tool)) {
if (isWriteTool(input.tool) && typeof output.output === "string" && shouldProcess(config)) {
await appendWriteHashlineOutput(output)
}
return
}
if (typeof output.output !== "string") {
return
}
if (!shouldProcess(config)) {
return
}
output.output = transformOutput(output.output)
},
}
}