From bd67419d1d62206cd37dc979cb01897375578e5d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 9 Dec 2025 18:02:45 +0900 Subject: [PATCH] feat(features): add hook message injector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Port hook-message-injector from opencode-cc-plugin (4 files) - constants.ts: XDG-based path definitions (MESSAGE_STORAGE, PART_STORAGE) - types.ts: MessageMeta, OriginalMessageContext, TextPart interfaces - injector.ts: injectHookMessage() implementation with message/part storage - index.ts: Barrel export - Self-contained module with no import path changes needed - Preserves XDG_DATA_HOME environment variable support - Preserves message fallback logic for incomplete originalMessage 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- .../hook-message-injector/constants.ts | 8 + src/features/hook-message-injector/index.ts | 2 + .../hook-message-injector/injector.ts | 141 ++++++++++++++++++ src/features/hook-message-injector/types.ts | 45 ++++++ 4 files changed, 196 insertions(+) create mode 100644 src/features/hook-message-injector/constants.ts create mode 100644 src/features/hook-message-injector/index.ts create mode 100644 src/features/hook-message-injector/injector.ts create mode 100644 src/features/hook-message-injector/types.ts diff --git a/src/features/hook-message-injector/constants.ts b/src/features/hook-message-injector/constants.ts new file mode 100644 index 0000000..ec25c05 --- /dev/null +++ b/src/features/hook-message-injector/constants.ts @@ -0,0 +1,8 @@ +import { join } from "node:path" +import { homedir } from "node:os" + +const xdgData = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share") + +export const OPENCODE_STORAGE = join(xdgData, "opencode", "storage") +export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message") +export const PART_STORAGE = join(OPENCODE_STORAGE, "part") diff --git a/src/features/hook-message-injector/index.ts b/src/features/hook-message-injector/index.ts new file mode 100644 index 0000000..a131b82 --- /dev/null +++ b/src/features/hook-message-injector/index.ts @@ -0,0 +1,2 @@ +export { injectHookMessage } from "./injector" +export type { MessageMeta, OriginalMessageContext, TextPart } from "./types" diff --git a/src/features/hook-message-injector/injector.ts b/src/features/hook-message-injector/injector.ts new file mode 100644 index 0000000..a790f20 --- /dev/null +++ b/src/features/hook-message-injector/injector.ts @@ -0,0 +1,141 @@ +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs" +import { join } from "node:path" +import { MESSAGE_STORAGE, PART_STORAGE } from "./constants" +import type { MessageMeta, OriginalMessageContext, TextPart } from "./types" + +interface StoredMessage { + agent?: string + model?: { providerID?: string; modelID?: string } + tools?: Record +} + +function findNearestMessageWithFields(messageDir: string): StoredMessage | null { + try { + const files = readdirSync(messageDir) + .filter((f) => f.endsWith(".json")) + .sort() + .reverse() + + for (const file of files) { + try { + const content = readFileSync(join(messageDir, file), "utf-8") + const msg = JSON.parse(content) as StoredMessage + if (msg.agent && msg.model?.providerID && msg.model?.modelID) { + return msg + } + } catch { + continue + } + } + } catch { + return null + } + return null +} + +function generateMessageId(): string { + const timestamp = Date.now().toString(16) + const random = Math.random().toString(36).substring(2, 14) + return `msg_${timestamp}${random}` +} + +function generatePartId(): string { + const timestamp = Date.now().toString(16) + const random = Math.random().toString(36).substring(2, 10) + return `prt_${timestamp}${random}` +} + +function getOrCreateMessageDir(sessionID: string): string { + if (!existsSync(MESSAGE_STORAGE)) { + mkdirSync(MESSAGE_STORAGE, { recursive: true }) + } + + const directPath = join(MESSAGE_STORAGE, sessionID) + if (existsSync(directPath)) { + return directPath + } + + for (const dir of readdirSync(MESSAGE_STORAGE)) { + const sessionPath = join(MESSAGE_STORAGE, dir, sessionID) + if (existsSync(sessionPath)) { + return sessionPath + } + } + + mkdirSync(directPath, { recursive: true }) + return directPath +} + +export function injectHookMessage( + sessionID: string, + hookContent: string, + originalMessage: OriginalMessageContext +): boolean { + const messageDir = getOrCreateMessageDir(sessionID) + + const needsFallback = + !originalMessage.agent || + !originalMessage.model?.providerID || + !originalMessage.model?.modelID + + const fallback = needsFallback ? findNearestMessageWithFields(messageDir) : null + + const now = Date.now() + const messageID = generateMessageId() + const partID = generatePartId() + + const resolvedAgent = originalMessage.agent ?? fallback?.agent ?? "general" + const resolvedModel = + originalMessage.model?.providerID && originalMessage.model?.modelID + ? { providerID: originalMessage.model.providerID, modelID: originalMessage.model.modelID } + : fallback?.model?.providerID && fallback?.model?.modelID + ? { providerID: fallback.model.providerID, modelID: fallback.model.modelID } + : undefined + const resolvedTools = originalMessage.tools ?? fallback?.tools + + const messageMeta: MessageMeta = { + id: messageID, + sessionID, + role: "user", + time: { + created: now, + }, + agent: resolvedAgent, + model: resolvedModel, + path: + originalMessage.path?.cwd + ? { + cwd: originalMessage.path.cwd, + root: originalMessage.path.root ?? "/", + } + : undefined, + tools: resolvedTools, + } + + const textPart: TextPart = { + id: partID, + type: "text", + text: hookContent, + synthetic: true, + time: { + start: now, + end: now, + }, + messageID, + sessionID, + } + + try { + writeFileSync(join(messageDir, `${messageID}.json`), JSON.stringify(messageMeta, null, 2)) + + const partDir = join(PART_STORAGE, messageID) + if (!existsSync(partDir)) { + mkdirSync(partDir, { recursive: true }) + } + writeFileSync(join(partDir, `${partID}.json`), JSON.stringify(textPart, null, 2)) + + return true + } catch { + return false + } +} diff --git a/src/features/hook-message-injector/types.ts b/src/features/hook-message-injector/types.ts new file mode 100644 index 0000000..165a83d --- /dev/null +++ b/src/features/hook-message-injector/types.ts @@ -0,0 +1,45 @@ +export interface MessageMeta { + id: string + sessionID: string + role: "user" | "assistant" + time: { + created: number + completed?: number + } + agent?: string + model?: { + providerID: string + modelID: string + } + path?: { + cwd: string + root: string + } + tools?: Record +} + +export interface OriginalMessageContext { + agent?: string + model?: { + providerID?: string + modelID?: string + } + path?: { + cwd?: string + root?: string + } + tools?: Record +} + +export interface TextPart { + id: string + type: "text" + text: string + synthetic: boolean + time: { + start: number + end: number + } + messageID: string + sessionID: string +}