feat(features): add hook message injector
- 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)
This commit is contained in:
8
src/features/hook-message-injector/constants.ts
Normal file
8
src/features/hook-message-injector/constants.ts
Normal file
@@ -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")
|
||||
2
src/features/hook-message-injector/index.ts
Normal file
2
src/features/hook-message-injector/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { injectHookMessage } from "./injector"
|
||||
export type { MessageMeta, OriginalMessageContext, TextPart } from "./types"
|
||||
141
src/features/hook-message-injector/injector.ts
Normal file
141
src/features/hook-message-injector/injector.ts
Normal file
@@ -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<string, boolean>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
45
src/features/hook-message-injector/types.ts
Normal file
45
src/features/hook-message-injector/types.ts
Normal file
@@ -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<string, boolean>
|
||||
}
|
||||
|
||||
export interface OriginalMessageContext {
|
||||
agent?: string
|
||||
model?: {
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
}
|
||||
path?: {
|
||||
cwd?: string
|
||||
root?: string
|
||||
}
|
||||
tools?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export interface TextPart {
|
||||
id: string
|
||||
type: "text"
|
||||
text: string
|
||||
synthetic: boolean
|
||||
time: {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
messageID: string
|
||||
sessionID: string
|
||||
}
|
||||
Reference in New Issue
Block a user