diff --git a/packages/omo-codex/plugin/scripts/build-components.mjs b/packages/omo-codex/plugin/scripts/build-components.mjs new file mode 100644 index 000000000..f054eab53 --- /dev/null +++ b/packages/omo-codex/plugin/scripts/build-components.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8")); +const workspaces = Array.isArray(packageJson.workspaces) ? packageJson.workspaces : []; + +for (const workspace of workspaces) { + if (typeof workspace !== "string" || !workspace.startsWith("components/")) continue; + const workspacePackageJson = JSON.parse(await readFile(join(root, workspace, "package.json"), "utf8")); + if (typeof workspacePackageJson.scripts?.build !== "string") continue; + + console.log(`Building ${workspace}`); + const result = spawnSync("bun", ["run", "--cwd", workspace, "build"], { + cwd: root, + stdio: "inherit", + }); + if (result.error !== undefined) throw result.error; + if (result.status !== 0) process.exit(result.status ?? 1); +} diff --git a/packages/omo-codex/plugin/scripts/hook-status-message.mjs b/packages/omo-codex/plugin/scripts/hook-status-message.mjs new file mode 100644 index 000000000..e7ba53caf --- /dev/null +++ b/packages/omo-codex/plugin/scripts/hook-status-message.mjs @@ -0,0 +1,46 @@ +const PRODUCT_NAME = "LazyCodex"; + +const WORD_OVERRIDES = new Map([ + ["lsp", "LSP"], + ["ulw-loop", "Ulw-Loop"], +]); + +export function formatLazyCodexHookStatusMessage(version, label) { + return `${PRODUCT_NAME}(${normalizeVersion(version)}): ${normalizeLazyCodexHookStatusLabel(label)}`; +} + +export function normalizeLazyCodexHookStatusLabel(label) { + const parsed = parseLazyCodexHookStatusMessage(label); + const rawLabel = parsed === null ? label : parsed.label; + const normalized = rawLabel.replace(/\bOMO\b/gi, " ").replace(/\s+/g, " ").trim(); + if (normalized.length === 0) return ""; + return normalized + .split(" ") + .map(formatWord) + .join(" "); +} + +export function parseLazyCodexHookStatusMessage(message) { + const match = /^LazyCodex\(([^)]+)\):\s+(.+)$/.exec(message.trim()); + if (match === null) return null; + const [, version, label] = match; + return { version, label }; +} + +function normalizeVersion(version) { + const normalized = version.trim(); + return normalized.length === 0 ? "local" : normalized; +} + +function formatWord(word) { + const lower = word.toLowerCase(); + const override = WORD_OVERRIDES.get(lower); + if (override !== undefined) return override; + if (word.includes("-")) { + return word + .split("-") + .map(formatWord) + .join("-"); + } + return `${lower.slice(0, 1).toUpperCase()}${lower.slice(1)}`; +} diff --git a/packages/omo-codex/plugin/scripts/sync-skills.mjs b/packages/omo-codex/plugin/scripts/sync-skills.mjs new file mode 100644 index 000000000..2ec48b630 --- /dev/null +++ b/packages/omo-codex/plugin/scripts/sync-skills.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { sharedSkillsRootPath } from "@oh-my-opencode/shared-skills"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const sharedSkillsRoot = sharedSkillsRootPath(); +const skillsRoot = join(root, "skills"); +const skillSources = [ + ["comment-checker", "components/comment-checker/skills/comment-checker"], + ["lsp", "components/lsp/skills/lsp"], + ["rules", "components/rules/skills/rules"], + ["ulw-loop", "components/ulw-loop/skills/ulw-loop"], +]; + +const opencodeOnlyOrchestrationPattern = /\b(?:call_omo_agent|background_output|team_[a-z_]+|task)\s*\(/; + +const codexHarnessToolCompatibility = `## Codex Harness Tool Compatibility + +This skill may include examples copied from the OpenCode harness. In Codex, do not call OpenCode-only tools such as \`call_omo_agent(...)\`, \`task(...)\`, \`background_output(...)\`, or \`team_*(...)\` literally. Translate those examples to Codex native tools: + +| OpenCode example | Codex tool to use | +| --- | --- | +| \`call_omo_agent(subagent_type="explore", ...)\` | \`spawn_agent(agent_type="explorer", task_name="...", message="...")\` | +| \`call_omo_agent(subagent_type="librarian", ...)\` | \`spawn_agent(agent_type="librarian", task_name="...", message="...")\` | +| \`task(subagent_type="plan", ...)\` | \`spawn_agent(agent_type="plan", task_name="...", message="...")\` | +| \`task(subagent_type="oracle", ...)\` for final verification | \`spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...")\` | +| \`task(category="...", ...)\` for implementation or QA | \`spawn_agent(agent_type="worker", task_name="...", message="...")\` | +| \`background_output(task_id="...")\` | \`wait_agent(...)\` to wait for subagent completion and mailbox updates | +| \`team_*(...)\` | Use Codex native subagents plus \`send_message\`, \`followup_task\`, \`wait_agent\`, and \`close_agent\` | + +When translating \`load_skills=[...]\`, include the requested skill names in the spawned agent's \`message\`. If a code block below conflicts with this section, this section wins. + +`; + +function insertCodexCompatibilityGuidance(content) { + if (!opencodeOnlyOrchestrationPattern.test(content)) return content; + if (content.includes("## Codex Harness Tool Compatibility")) return content; + + const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n+/); + if (!frontmatterMatch) { + return `${codexHarnessToolCompatibility}${content}`; + } + + return `${frontmatterMatch[0]}${codexHarnessToolCompatibility}${content.slice(frontmatterMatch[0].length)}`; +} + +async function adaptSkillForCodex(skillName) { + const skillPath = join(skillsRoot, skillName, "SKILL.md"); + const content = await readFile(skillPath, "utf8"); + const adapted = insertCodexCompatibilityGuidance(content); + if (adapted !== content) { + await writeFile(skillPath, adapted, "utf8"); + } +} + +await rm(skillsRoot, { recursive: true, force: true }); +await mkdir(skillsRoot, { recursive: true }); + +for (const [name, source] of skillSources) { + await cp(join(root, source), join(skillsRoot, name), { recursive: true }); + await adaptSkillForCodex(name); +} + +const sharedSkillEntries = await readdir(sharedSkillsRoot, { withFileTypes: true }); +const sharedSkillNames = sharedSkillEntries + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + +for (const skillName of sharedSkillNames) { + await cp(join(sharedSkillsRoot, skillName), join(skillsRoot, skillName), { recursive: true }); + await adaptSkillForCodex(skillName); +}