Skip to content

Commit f8dbfb0

Browse files
aanishsclaude
andauthored
feat: Codex host support (#42)
* feat: Codex host support — multi-host setup and skill generation Add --host flag to setup script supporting both Claude and Codex targets. Codex gets its own skill directory (~/.codex/skills) with host-appropriate skill files. Includes generation, setup, and smoke tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: implement --host codex in gen-skill-docs, fix setup bugs, add tests to CI - Implement Codex generation path in gen-skill-docs.ts: --host flag, frontmatter stripping, .claude→.codex/.agents path rewriting, agents/openai.yaml emission, dry-run support - Fix stale CODEX_GENERATED_SKILLS_DIR after migrate_direct_codex_install - Remove unused emdash_dir param from link_codex_skill_dirs - Pass EMDASH_CODEX_OUTPUT_ROOT in smoke test to avoid source tree pollution - Add codex test files to package.json test script Addresses: P0 missing codex generator, P1 stale path after migration, P1 tests not in CI, P2 smoke test pollution, P2 dead parameter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fa57f88 commit f8dbfb0

6 files changed

Lines changed: 612 additions & 70 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"scripts": {
1212
"build": "bun run gen:skill-docs",
1313
"gen:skill-docs": "bun run scripts/gen-skill-docs.ts",
14-
"test": "bun test test/skill-validation.test.ts test/rego-policy.test.ts test/bin-smoke.test.ts test/touchfiles.test.ts test/architecture.test.ts test/comply-start.test.ts",
14+
"test": "bun test test/skill-validation.test.ts test/rego-policy.test.ts test/bin-smoke.test.ts test/touchfiles.test.ts test/architecture.test.ts test/comply-start.test.ts test/codex-generation.test.ts test/codex-setup.test.ts test/codex-smoke.test.ts",
1515
"test:evals": "EVALS=1 bun test test/skill-e2e.test.ts",
1616
"test:evals:all": "EVALS_ALL=1 bun test test/skill-e2e.test.ts",
1717
"skill:check": "bun run scripts/skill-check.ts",

scripts/gen-skill-docs.ts

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import type { FrameworkDefinition } from "../frameworks/schema";
1414

1515
const ROOT = path.resolve(import.meta.dir, "..");
1616
const DRY_RUN = process.argv.includes("--dry-run");
17+
const HOST_IDX = process.argv.indexOf("--host");
18+
const HOST =
19+
HOST_IDX !== -1 ? (process.argv[HOST_IDX + 1] ?? "claude") : "claude";
20+
const CODEX_OUTPUT_ROOT =
21+
process.env.EMDASH_CODEX_OUTPUT_ROOT ?? path.join(ROOT, ".agents", "skills");
1722

1823
// ─── Framework Loading ──────────────────────────────────────
1924

@@ -1765,25 +1770,115 @@ function findTemplates(): string[] {
17651770
return templates;
17661771
}
17671772

1773+
// ─── Codex Post-Processing ─────────────────────────────────
1774+
1775+
function stripCodexFrontmatter(content: string): string {
1776+
const fmStart = content.indexOf("---");
1777+
const fmEnd = content.indexOf("---", fmStart + 3);
1778+
if (fmStart === -1 || fmEnd === -1) return content;
1779+
1780+
const before = content.slice(0, fmStart + 4);
1781+
const fm = content.slice(fmStart + 4, fmEnd);
1782+
const after = content.slice(fmEnd);
1783+
1784+
let cleaned = fm.replace(/^version:.*\n/m, "");
1785+
cleaned = cleaned.replace(/^allowed-tools:\n( - .*\n)*/m, "");
1786+
1787+
return before + cleaned + after;
1788+
}
1789+
1790+
function replaceClaudePaths(content: string): string {
1791+
return content
1792+
.replace(/~\/\.claude\/skills\/em-dash/g, "~/.codex/skills/em-dash")
1793+
.replace(/\.claude\/skills\/em-dash/g, ".agents/skills/em-dash");
1794+
}
1795+
1796+
function extractFrontmatterField(content: string, field: string): string {
1797+
const fmStart = content.indexOf("---");
1798+
const fmEnd = content.indexOf("---", fmStart + 3);
1799+
if (fmStart === -1 || fmEnd === -1) return "";
1800+
const fm = content.slice(fmStart + 4, fmEnd);
1801+
1802+
if (field === "description") {
1803+
const multiLine = fm.match(/^description:\s*\|?\s*\n((?: .*\n)*)/m);
1804+
if (multiLine) return multiLine[1].replace(/^ /gm, "").trim();
1805+
const inlineMatch = fm.match(/^description:\s*(.+)$/m);
1806+
return inlineMatch ? inlineMatch[1].trim() : "";
1807+
}
1808+
1809+
const match = fm.match(new RegExp(`^${field}:\\s*(.+)$`, "m"));
1810+
return match ? match[1].trim() : "";
1811+
}
1812+
1813+
function generateOpenAIYaml(skillName: string, description: string): string {
1814+
let shortDesc = description.replace(/\n/g, " ").trim();
1815+
if (shortDesc.length > 120) shortDesc = `${shortDesc.slice(0, 117)}...`;
1816+
const escaped = shortDesc.replace(/"/g, '\\"');
1817+
return [
1818+
"interface:",
1819+
` display_name: "${skillName}"`,
1820+
` short_description: "${escaped}"`,
1821+
` default_prompt: "Use ${skillName} for this task."`,
1822+
"policy:",
1823+
" allow_implicit_invocation: true",
1824+
"",
1825+
].join("\n");
1826+
}
1827+
1828+
// ─── Generation Loop ───────────────────────────────────────
1829+
17681830
let hasChanges = false;
17691831

17701832
for (const tmplPath of findTemplates()) {
1771-
const { outputPath, content } = processTemplate(tmplPath);
1772-
const relOutput = path.relative(ROOT, outputPath);
1773-
1774-
if (DRY_RUN) {
1775-
const existing = fs.existsSync(outputPath)
1776-
? fs.readFileSync(outputPath, "utf-8")
1777-
: "";
1778-
if (existing !== content) {
1779-
console.log(`STALE: ${relOutput}`);
1780-
hasChanges = true;
1833+
const { outputPath: claudeOutputPath, content: claudeContent } =
1834+
processTemplate(tmplPath);
1835+
1836+
if (HOST === "claude") {
1837+
const relOutput = path.relative(ROOT, claudeOutputPath);
1838+
if (DRY_RUN) {
1839+
const existing = fs.existsSync(claudeOutputPath)
1840+
? fs.readFileSync(claudeOutputPath, "utf-8")
1841+
: "";
1842+
if (existing !== claudeContent) {
1843+
console.log(`STALE: ${relOutput}`);
1844+
hasChanges = true;
1845+
} else {
1846+
console.log(`FRESH: ${relOutput}`);
1847+
}
17811848
} else {
1782-
console.log(`FRESH: ${relOutput}`);
1849+
fs.writeFileSync(claudeOutputPath, claudeContent);
1850+
console.log(`GENERATED: ${relOutput}`);
17831851
}
17841852
} else {
1785-
fs.writeFileSync(outputPath, content);
1786-
console.log(`GENERATED: ${relOutput}`);
1853+
const content = replaceClaudePaths(stripCodexFrontmatter(claudeContent));
1854+
const skillName = path.basename(path.dirname(tmplPath));
1855+
const skillDir = path.join(CODEX_OUTPUT_ROOT, skillName);
1856+
const outputPath = path.join(skillDir, "SKILL.md");
1857+
const yamlPath = path.join(skillDir, "agents", "openai.yaml");
1858+
const relOutput = path.relative(CODEX_OUTPUT_ROOT, outputPath);
1859+
1860+
const description = extractFrontmatterField(claudeContent, "description");
1861+
const yamlContent = generateOpenAIYaml(skillName, description);
1862+
1863+
if (DRY_RUN) {
1864+
const existingMd = fs.existsSync(outputPath)
1865+
? fs.readFileSync(outputPath, "utf-8")
1866+
: "";
1867+
const existingYaml = fs.existsSync(yamlPath)
1868+
? fs.readFileSync(yamlPath, "utf-8")
1869+
: "";
1870+
if (existingMd !== content || existingYaml !== yamlContent) {
1871+
console.log(`STALE: ${relOutput}`);
1872+
hasChanges = true;
1873+
} else {
1874+
console.log(`FRESH: ${relOutput}`);
1875+
}
1876+
} else {
1877+
fs.mkdirSync(path.join(skillDir, "agents"), { recursive: true });
1878+
fs.writeFileSync(outputPath, content);
1879+
fs.writeFileSync(yamlPath, yamlContent);
1880+
console.log(`GENERATED: ${relOutput}`);
1881+
}
17871882
}
17881883
}
17891884

0 commit comments

Comments
 (0)