diff --git a/packages/omo-codex/plugin/components/ultrawork/src/cli.ts b/packages/omo-codex/plugin/components/ultrawork/src/cli.ts new file mode 100644 index 000000000..6b9d71fb0 --- /dev/null +++ b/packages/omo-codex/plugin/components/ultrawork/src/cli.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env node +import { stdin as processStdin, stdout as processStdout } from "node:process"; + +import { runUserPromptSubmitHook } from "./codex-hook.js"; + +const command = process.argv[2]; +const subcommand = process.argv[3]; + +if (command === "hook" && subcommand === "user-prompt-submit") { + await runHookCli(); +} else { + process.stderr.write("Usage: omo-ultrawork hook user-prompt-submit\n"); + process.exitCode = 1; +} + +async function runHookCli(): Promise { + const raw = await readStdin(); + if (raw.trim().length === 0) return; + const parsed = parseHookInput(raw); + const output = runUserPromptSubmitHook(parsed); + if (output.length > 0) { + processStdout.write(output); + } +} + +function parseHookInput(raw: string): unknown | undefined { + try { + const parsed: unknown = JSON.parse(raw); + return parsed; + } catch (error) { + if (error instanceof SyntaxError) return undefined; + throw error; + } +} + +function readStdin(): Promise { + return new Promise((resolve) => { + let data = ""; + processStdin.setEncoding("utf8"); + processStdin.on("data", (chunk: string) => { + data += chunk; + }); + processStdin.once("error", () => { + resolve(data); + }); + processStdin.once("end", () => { + resolve(data); + }); + }); +} diff --git a/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts b/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts new file mode 100644 index 000000000..49317db9a --- /dev/null +++ b/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts @@ -0,0 +1,84 @@ +import { readFileSync } from "node:fs"; + +import { ULTRAWORK_DIRECTIVE } from "./directive.js"; + +const ULTRAWORK_PATTERN = /\b(?:ultrawork|ulw)\b/i; +const CONTEXT_PRESSURE_MARKERS = [ + "context compacted", + "context_length_exceeded", + "skill descriptions were shortened", + "context_too_large", + "codex ran out of room in the model's context window", + "your input exceeds the context window", + "long threads and multiple compactions", +] as const; + +export type CodexUserPromptSubmitInput = { + readonly hook_event_name: "UserPromptSubmit"; + readonly prompt: string; + readonly transcript_path?: string | null; +}; + +interface UserPromptSubmitHookOutput { + readonly hookSpecificOutput: { + readonly hookEventName: "UserPromptSubmit"; + readonly additionalContext: string; + }; +} + +export function runUserPromptSubmitHook(input: unknown): string { + if (!isCodexUserPromptSubmitInput(input)) return ""; + if (isContextPressureRecoveryPrompt(input.prompt)) return ""; + if (isContextPressureTranscript(input.transcript_path)) return ""; + return isUltraworkPrompt(input.prompt) ? formatAdditionalContextOutput(ULTRAWORK_DIRECTIVE) : ""; +} + +export function isUltraworkPrompt(prompt: string): boolean { + return ULTRAWORK_PATTERN.test(prompt); +} + +function isContextPressureRecoveryPrompt(prompt: string): boolean { + const normalizedPrompt = prompt.toLowerCase(); + return CONTEXT_PRESSURE_MARKERS.some((marker) => normalizedPrompt.includes(marker)); +} + +function isContextPressureTranscript(transcriptPath: string | null | undefined): boolean { + if (transcriptPath === undefined || transcriptPath === null) return false; + try { + return isContextPressureRecoveryPrompt(readFileSync(transcriptPath, "utf8")); + } catch (error) { + if (error instanceof Error) return false; + throw error; + } +} + +function formatAdditionalContextOutput(additionalContext: string): string { + const normalizedContext = normalizeAdditionalContext(additionalContext); + if (normalizedContext.length === 0) return ""; + const output: UserPromptSubmitHookOutput = { + hookSpecificOutput: { + hookEventName: "UserPromptSubmit", + additionalContext: normalizedContext, + }, + }; + return `${JSON.stringify(output)}\n`; +} + +function normalizeAdditionalContext(additionalContext: string): string { + return additionalContext.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim(); +} + +function isCodexUserPromptSubmitInput(value: unknown): value is CodexUserPromptSubmitInput { + return ( + isRecord(value) && + value["hook_event_name"] === "UserPromptSubmit" && + typeof value["prompt"] === "string" && + (value["transcript_path"] === undefined || + value["transcript_path"] === null || + typeof value["transcript_path"] === "string") + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/omo-codex/plugin/components/ultrawork/src/directive.ts b/packages/omo-codex/plugin/components/ultrawork/src/directive.ts new file mode 100644 index 000000000..faf60ae6a --- /dev/null +++ b/packages/omo-codex/plugin/components/ultrawork/src/directive.ts @@ -0,0 +1,3 @@ +import { readFileSync } from "node:fs"; + +export const ULTRAWORK_DIRECTIVE: string = readFileSync(new URL("../directive.md", import.meta.url), "utf8");