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