feat(prompts-core): support bundled prompt sources

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-24 18:28:57 +09:00
parent b77fed28ef
commit 91351d73fd
3 changed files with 120 additions and 7 deletions
+58 -2
View File
@@ -1,7 +1,14 @@
import { parseFrontmatter } from "@oh-my-opencode/utils"
import { readFile } from "node:fs/promises"
import { isAbsolute, relative, resolve } from "node:path"
import type { LoadedPrompt, LoadPromptInput, RuntimeInjection } from "./types"
import type {
LoadedPrompt,
LoadBundledPromptInput,
LoadFilesystemPromptInput,
LoadPromptInput,
RuntimeInjection,
SyncRuntimeInjection,
} from "./types"
export class PromptFileNotFoundError extends Error {
readonly name = "PromptFileNotFoundError"
@@ -27,8 +34,31 @@ export class PromptPathTraversalError extends Error {
}
}
export async function loadPrompt<TFrontmatter = Record<string, unknown>>(
export function loadPrompt<TFrontmatter = Record<string, unknown>>(
input: LoadBundledPromptInput
): LoadedPrompt<TFrontmatter>
export function loadPrompt<TFrontmatter = Record<string, unknown>>(
input: LoadFilesystemPromptInput
): Promise<LoadedPrompt<TFrontmatter>>
export function loadPrompt<TFrontmatter = Record<string, unknown>>(
input: LoadPromptInput
): LoadedPrompt<TFrontmatter> | Promise<LoadedPrompt<TFrontmatter>> {
if (isLoadBundledPromptInput(input)) return loadBundledPrompt(input)
return loadFilesystemPrompt(input)
}
export function loadPromptSync<TFrontmatter = Record<string, unknown>>(
input: LoadBundledPromptInput
): LoadedPrompt<TFrontmatter> {
return loadBundledPrompt(input)
}
function isLoadBundledPromptInput(input: LoadPromptInput): input is LoadBundledPromptInput {
return input.source.kind === "bundled"
}
async function loadFilesystemPrompt<TFrontmatter = Record<string, unknown>>(
input: LoadFilesystemPromptInput
): Promise<LoadedPrompt<TFrontmatter>> {
const filePath = resolvePromptFilePath(input.source.baseDir, input.name, input.variant)
const content = await readPromptFile(input.name, input.variant, filePath)
@@ -44,6 +74,21 @@ export async function loadPrompt<TFrontmatter = Record<string, unknown>>(
}
}
function loadBundledPrompt<TFrontmatter = Record<string, unknown>>(
input: LoadBundledPromptInput
): LoadedPrompt<TFrontmatter> {
const parsed = parseFrontmatter<TFrontmatter>(input.source.content)
const body = applyRuntimeInjectionsSync(parsed.body, input.inject ?? [])
return {
frontmatter: parsed.data,
body,
hadFrontmatter: parsed.hadFrontmatter,
parseError: parsed.parseError,
filePath: input.source.filePath,
}
}
function resolvePromptFilePath(baseDir: string, promptName: string, variant: string): string {
const resolvedBaseDir = resolve(baseDir)
const filePath = resolve(resolvedBaseDir, promptName, `${variant}.md`)
@@ -76,6 +121,17 @@ async function applyRuntimeInjections(
return renderedBody
}
function applyRuntimeInjectionsSync(
body: string,
injections: readonly SyncRuntimeInjection[]
): string {
let renderedBody = body
for (const injection of injections) {
renderedBody = renderedBody.replaceAll(injection.placeholder, injection.resolver())
}
return renderedBody
}
function getErrorCode(error: Error): string | undefined {
if (!("code" in error)) return undefined
return typeof error.code === "string" ? error.code : undefined