@khanhcan148/mk 0.1.21 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,8 +35,10 @@ mk init --dry-run
35
35
  mk init Install kit files to current project (./.claude/)
36
36
  mk init --global Install kit globally (~/.claude/)
37
37
  mk init --dry-run Preview what would be installed
38
- mk update Update kit files (preserves your edits by default)
38
+ mk update Update kit files (preserves your edits by default); auto-syncs .codex/ mirror when present
39
39
  mk update --force Update and overwrite user-modified files
40
+ mk codex Mirror .claude/ to .codex/ for OpenAI Codex CLI (see docs/codex/COMMAND.md)
41
+ mk codex --cwd DIR Convert a project at a different path
40
42
  mk remove Remove all kit files tracked by manifest
41
43
  mk --version Show installed CLI version
42
44
  mk --help Show help
@@ -89,7 +91,7 @@ cp -r .claude ~/.claude/
89
91
  ```
90
92
  ├── .claude/
91
93
  │ ├── agents/ # 36 agents (5 primary + 31 utility: implementers, quality, docs, specialized, concerns, brainstorm critics)
92
- │ ├── skills/ # 67 skill packages (SKILL.md + scripts/references/assets)
94
+ │ ├── skills/ # 65 skill packages (SKILL.md + scripts/references/assets)
93
95
  │ │ ├── mk-*/ # 20 workflow commands (/mk-audit, /mk-brainstorm, /mk-log-analysis, /mk-overview, /mk-wiki, etc.)
94
96
  │ │ └── ... # Domain skills (frontend, backend, testing, browser automation, etc.)
95
97
  │ └── workflows/ # Development protocols
@@ -228,6 +230,8 @@ python .claude/skills/skill-creator/scripts/package_skill.py <path/to/skill-fold
228
230
  |----------|-------------|
229
231
  | [Skill Index](docs/skill-index.md) | Categorized list of all skills and workflows |
230
232
  | [mk-* Workflows & Agents](docs/mk-workflow-agents.md) | How each /mk-* command works and which agents it uses |
233
+ | [`mk codex` Command Guide](docs/codex/COMMAND.md) | Convert .claude/ to .codex/ for OpenAI Codex CLI; auto-sync on mk update |
234
+ | [Codex Migration Guide](docs/codex/MIGRATION.md) | What ports, what's dropped, what gaps remain |
231
235
  | [Project Overview & PDR](docs/project-overview-pdr.md) | Requirements, constraints, success metrics |
232
236
  | [Project Roadmap](docs/project-roadmap.md) | Phases, milestones, and risk register |
233
237
  | [Changelog](docs/changelog.md) | Version history and release notes |
@@ -259,4 +263,6 @@ This kit is designed to work on Windows, macOS, and Linux:
259
263
  - **Scripts**: All executable scripts use Python or Node.js (no bash/shell scripts)
260
264
  - **Claude Code Bash tool**: Provides a Unix-like shell on all platforms, including Windows — git commands and other Bash tool invocations work everywhere
261
265
  - **External tool installs**: Reference files include install instructions for macOS (`brew`), Ubuntu/Debian (`apt-get`), and Windows (`winget`/`choco`) where applicable
262
- **Supported runtime**: Claude Code CLI. All skills, agents, and workflows are designed for the Claude Code runtime.
266
+ **Supported runtimes**:
267
+ - **Claude Code CLI** (primary): all skills, agents, and workflows are authored for this runtime.
268
+ - **OpenAI Codex CLI** (via `mk codex`): the `.claude/` tree is converted to a `.codex/` mirror — agents become TOML, skills/workflows/hooks are mirrored with kit-paths rewritten, and a `config.toml` is emitted with hook registrations. See [`mk codex` Command Guide](docs/codex/COMMAND.md). After conversion, `mk update` auto-syncs the `.codex/` mirror so it stays coherent.
package/bin/mk.js CHANGED
@@ -7,6 +7,7 @@ import { initAction } from '../src/commands/init.js';
7
7
  import { updateAction } from '../src/commands/update.js';
8
8
  import { removeAction } from '../src/commands/remove.js';
9
9
  import { loginAction, logoutAction, statusAction } from '../src/commands/auth.js';
10
+ import { codexAction } from '../src/commands/codex.js';
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -33,6 +34,13 @@ program.command('remove')
33
34
  .option('--global', 'Remove global installation from ~/.claude/')
34
35
  .action((options) => removeAction(options));
35
36
 
37
+ program.command('codex')
38
+ .description('Mirror .claude/ to .codex/ for OpenAI Codex CLI (agents converted to TOML; skills + workflows copied)')
39
+ .option('--cwd <dir>', 'Project directory (default: current working directory)')
40
+ .option('--output <dir>', 'Output directory (default: <cwd>/.codex/agents)')
41
+ .option('--model-map <toml>', 'Path to a TOML file with [model_map] overrides')
42
+ .action((options) => codexAction(options));
43
+
36
44
  // Auth command group
37
45
  const auth = program.command('auth')
38
46
  .description('Manage GitHub authentication for kit downloads');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanhcan148/mk",
3
- "version": "0.1.21",
3
+ "version": "0.1.24",
4
4
  "description": "CLI to install and manage MyClaudeKit (.claude/) in your projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,22 +8,28 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "src/"
11
+ "src/",
12
+ "scripts/"
12
13
  ],
13
14
  "engines": {
14
15
  "node": ">=18.0.0"
15
16
  },
16
17
  "scripts": {
17
- "test": "node --test test/lib/*.test.js test/commands/*.test.js test/integration/*.test.js test/characterization/*.characterization.test.js .claude/hooks/tests/*.test.cjs",
18
+ "test": "node --test test/lib/*.test.js test/commands/*.test.js test/scripts/*.test.js test/integration/*.test.js test/characterization/*.characterization.test.js .claude/hooks/tests/*.test.cjs",
18
19
  "lint": "node --check src/**/*.js bin/**/*.js 2>/dev/null",
19
- "selftest": "python3 .claude/skills/mk-selftest/scripts/validate_kit.py"
20
+ "selftest": "python3 .claude/skills/mk-selftest/scripts/validate_kit.py",
21
+ "codex:convert": "node bin/mk.js codex",
22
+ "codex:convert-and-diff": "node scripts/codex-diff-check.js"
20
23
  },
21
24
  "dependencies": {
25
+ "@iarna/toml": "3.0.0",
22
26
  "chalk": "^5.0.0",
23
27
  "commander": "^12.0.0",
24
28
  "fs-extra": "^11.0.0",
29
+ "js-yaml": "4.1.0",
25
30
  "semver": "^7.0.0"
26
31
  },
32
+ "devDependencies": {},
27
33
  "keywords": [
28
34
  "claude",
29
35
  "claude-code",
File without changes
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * codex-diff-check.js
4
+ *
5
+ * Runs `mk codex` twice with separate output dirs and asserts byte-identical
6
+ * config.toml output. Exits 0 on match, 1 on mismatch.
7
+ *
8
+ * Used by `npm run codex:convert-and-diff` in CI to enforce the determinism
9
+ * contract (HC #4) without depending on platform-specific `diff` command.
10
+ */
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import { mkdtempSync, readFileSync, rmSync, existsSync, mkdirSync } from 'node:fs';
14
+ import { join, resolve, dirname } from 'node:path';
15
+ import { tmpdir } from 'node:os';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const PACKAGE_ROOT = resolve(__dirname, '..');
20
+ const MK_BIN = join(PACKAGE_ROOT, 'bin', 'mk.js');
21
+
22
+ function runMkCodex(agentsOut) {
23
+ const result = spawnSync(process.execPath, [MK_BIN, 'codex', '--output', agentsOut], {
24
+ stdio: 'inherit',
25
+ timeout: 60_000,
26
+ });
27
+ return result.status ?? 1;
28
+ }
29
+
30
+ const run1 = mkdtempSync(join(tmpdir(), 'mk-codex-diff1-'));
31
+ const run2 = mkdtempSync(join(tmpdir(), 'mk-codex-diff2-'));
32
+
33
+ try {
34
+ const status1 = runMkCodex(join(run1, 'agents'));
35
+ if (status1 !== 0) {
36
+ process.stderr.write(`[codex-diff-check] Run 1 failed with exit ${status1}\n`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const status2 = runMkCodex(join(run2, 'agents'));
41
+ if (status2 !== 0) {
42
+ process.stderr.write(`[codex-diff-check] Run 2 failed with exit ${status2}\n`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const toml1Path = join(run1, 'config.toml');
47
+ const toml2Path = join(run2, 'config.toml');
48
+
49
+ if (!existsSync(toml1Path) || !existsSync(toml2Path)) {
50
+ process.stderr.write('[codex-diff-check] config.toml missing from one or both runs\n');
51
+ process.exit(1);
52
+ }
53
+
54
+ const toml1 = readFileSync(toml1Path);
55
+ const toml2 = readFileSync(toml2Path);
56
+
57
+ if (!toml1.equals(toml2)) {
58
+ process.stderr.write('[codex-diff-check] FAIL: config.toml is NOT byte-identical across two runs\n');
59
+ process.stderr.write(` Run 1 (${toml1.length} bytes):\n${toml1.toString()}\n`);
60
+ process.stderr.write(` Run 2 (${toml2.length} bytes):\n${toml2.toString()}\n`);
61
+ process.exit(1);
62
+ }
63
+
64
+ process.stdout.write('[codex-diff-check] PASS: config.toml is byte-identical across two runs\n');
65
+ process.exit(0);
66
+ } finally {
67
+ rmSync(run1, { recursive: true, force: true });
68
+ rmSync(run2, { recursive: true, force: true });
69
+ }
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * convert-agents-to-codex.js
4
+ *
5
+ * Converts Claude Code agent Markdown files (.claude/agents/<name>.md) into
6
+ * OpenAI Codex CLI TOML sub-agent files (.codex/agents/<name>.toml).
7
+ *
8
+ * Usage:
9
+ * node scripts/convert-agents-to-codex.js [--input <dir>] [--output <dir>] [--model-map <toml>]
10
+ *
11
+ * Options:
12
+ * --input <dir> Directory containing source .md agent files.
13
+ * Default: <kit-root>/.claude/agents
14
+ * --output <dir> Output directory for .toml files.
15
+ * Default: <input-parent>/.codex/agents (or <kit-root>/.codex/agents)
16
+ * --model-map <toml> Path to a TOML file with a [model_map] table that
17
+ * overrides the built-in MODEL_MAP. Optional.
18
+ *
19
+ * Compliance
20
+ * ----------
21
+ * IAC-1: Scans agent bodies for imperative Claude-only constructs
22
+ * (AskUserQuestion, Agent(, TodoWrite, model: opus) and emits
23
+ * per-occurrence warnings to stderr. Non-fatal.
24
+ * IAC-3: Requires TOOL_DIR_NAME to be defined in src/lib/constants.js.
25
+ * Exits 1 if undefined (parent path-abstraction PR not yet merged).
26
+ * Override with env var MK_FORCE_NO_TOOL_DIR=1 for testing only.
27
+ * IAC-4: Strips KIT_INTERNAL_SKILLS from skills.config arrays.
28
+ * R2: Pre-emit triple-quote escape + post-emit round-trip parse assertion.
29
+ * developer_instructions must survive byte-for-byte.
30
+ */
31
+
32
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
33
+ import { join, resolve, basename, dirname } from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+ import toml from '@iarna/toml';
36
+ import yaml from 'js-yaml';
37
+ import { loadModelMap, resolveModel } from '../src/lib/runtime-codex.js';
38
+ import { KIT_INTERNAL_SKILLS } from '../src/lib/constants.js';
39
+ import { rewriteKitPaths } from '../src/lib/codex-rewrite.js';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // IAC-3: TOOL_DIR_NAME gate
43
+ // ---------------------------------------------------------------------------
44
+
45
+ let TOOL_DIR_NAME;
46
+ if (process.env.MK_FORCE_NO_TOOL_DIR === '1') {
47
+ // Test-only escape hatch: simulate missing constant
48
+ process.stderr.write(
49
+ '[convert-agents-to-codex] ERROR: TOOL_DIR_NAME is undefined.\n' +
50
+ 'The parent path-abstraction PR has not yet merged into src/lib/constants.js.\n' +
51
+ 'Merge that PR first, then re-run the converter.\n' +
52
+ 'Spike reference: docs/spikes/codex-20260509-1801/codex-spike.md\n'
53
+ );
54
+ process.exit(1);
55
+ }
56
+
57
+ try {
58
+ ({ TOOL_DIR_NAME } = await import('../src/lib/constants.js'));
59
+ if (!TOOL_DIR_NAME) {
60
+ throw new Error('TOOL_DIR_NAME is falsy');
61
+ }
62
+ } catch (err) {
63
+ process.stderr.write(
64
+ '[convert-agents-to-codex] ERROR: Cannot import TOOL_DIR_NAME from src/lib/constants.js.\n' +
65
+ `Details: ${err.message}\n` +
66
+ 'The parent path-abstraction PR has not yet merged.\n' +
67
+ 'Spike reference: docs/spikes/codex-20260509-1801/codex-spike.md\n'
68
+ );
69
+ process.exit(1);
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // CLI argument parsing
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const args = process.argv.slice(2);
77
+ let inputDir;
78
+ let outputDir;
79
+ let modelMapPath;
80
+
81
+ for (let i = 0; i < args.length; i++) {
82
+ if (args[i] === '--input' && args[i + 1]) {
83
+ inputDir = resolve(args[++i]);
84
+ } else if (args[i] === '--output' && args[i + 1]) {
85
+ outputDir = resolve(args[++i]);
86
+ } else if (args[i] === '--model-map' && args[i + 1]) {
87
+ modelMapPath = resolve(args[++i]);
88
+ }
89
+ }
90
+
91
+ const kitRoot = resolve(fileURLToPath(import.meta.url), '..', '..');
92
+ const agentsDir = inputDir || join(kitRoot, TOOL_DIR_NAME, 'agents');
93
+ if (!outputDir) {
94
+ // When --input is given, derive output sibling-of-input; else fall back to kit root.
95
+ outputDir = inputDir
96
+ ? join(dirname(agentsDir), '..', '.codex', 'agents')
97
+ : join(kitRoot, '.codex', 'agents');
98
+ outputDir = resolve(outputDir);
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Load model map
103
+ // ---------------------------------------------------------------------------
104
+
105
+ let modelMap;
106
+ try {
107
+ modelMap = loadModelMap(modelMapPath);
108
+ } catch (err) {
109
+ process.stderr.write(`[convert-agents-to-codex] ERROR: ${err.message}\n`);
110
+ process.exit(1);
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // IAC-1 patterns: Claude-only constructs to flag in agent bodies
115
+ // ---------------------------------------------------------------------------
116
+
117
+ const IAC1_PATTERNS = [
118
+ { pattern: /AskUserQuestion/g, label: 'AskUserQuestion' },
119
+ { pattern: /Agent\(/g, label: 'Agent(' },
120
+ { pattern: /TodoWrite/g, label: 'TodoWrite' },
121
+ { pattern: /model:\s*opus/g, label: 'model: opus' },
122
+ ];
123
+
124
+ /**
125
+ * Scan agent body for IAC-1 constructs and emit per-occurrence warnings.
126
+ * @param {string} agentName
127
+ * @param {string} body Raw Markdown body (frontmatter stripped)
128
+ */
129
+ function checkIac1(agentName, body) {
130
+ const lines = body.split('\n');
131
+ for (const { pattern, label } of IAC1_PATTERNS) {
132
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
133
+ const line = lines[lineIdx];
134
+ let match;
135
+ // Reset lastIndex for global patterns each outer pass
136
+ pattern.lastIndex = 0;
137
+ while ((match = pattern.exec(line)) !== null) {
138
+ process.stderr.write(
139
+ `[IAC-1] ${agentName}: "${label}" at line ${lineIdx + 1} col ${match.index + 1} — ` +
140
+ `this construct has no Codex equivalent; agent may need manual adaptation.\n`
141
+ );
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Frontmatter parser
149
+ // ---------------------------------------------------------------------------
150
+
151
+ /**
152
+ * Split a Markdown file into frontmatter and body.
153
+ * @param {string} content Raw file content
154
+ * @returns {{ frontmatter: object, body: string }}
155
+ */
156
+ function parseMd(content) {
157
+ const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
158
+ const match = content.match(fmRegex);
159
+ if (!match) {
160
+ return { frontmatter: {}, body: content };
161
+ }
162
+ let frontmatter;
163
+ try {
164
+ frontmatter = yaml.load(match[1]) || {};
165
+ } catch (err) {
166
+ frontmatter = {};
167
+ }
168
+ return { frontmatter, body: match[2] };
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // IAC-4: filter internal skills from skills.config
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * Filter out KIT_INTERNAL_SKILLS from a skills array.
177
+ * @param {string[]} skills
178
+ * @returns {string[]}
179
+ */
180
+ function filterSkills(skills) {
181
+ if (!Array.isArray(skills)) return [];
182
+ return skills.filter((s) => !KIT_INTERNAL_SKILLS.includes(s));
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Path rewrite: .claude/{workflows,skills,agents,commands,hooks}/ → .codex/{...}/
187
+ // Delegated to shared helper in src/lib/codex-rewrite.js (imported above).
188
+ // ---------------------------------------------------------------------------
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // R2: triple-quote escape
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Escape sequences of 3+ double-quotes within a string to prevent them from
196
+ * accidentally terminating TOML multi-line basic strings.
197
+ * Strategy: insert a TOML line-ending backslash continuation before each run
198
+ * of 3+ double-quotes, splitting them into safe pairs.
199
+ *
200
+ * @param {string} text
201
+ * @returns {string}
202
+ */
203
+ function escapeTripleQuotes(text) {
204
+ return text.replace(/"""/g, '""\\"');
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Converter core
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Convert a single agent .md file to a Codex TOML object.
213
+ * @param {string} mdPath Absolute path to the .md file
214
+ * @returns {{ name: string, tomlStr: string }}
215
+ */
216
+ function convertAgent(mdPath) {
217
+ const raw = readFileSync(mdPath, 'utf8');
218
+ const agentName = basename(mdPath, '.md');
219
+ const { frontmatter, body } = parseMd(raw);
220
+
221
+ // IAC-1 scan
222
+ checkIac1(agentName, body);
223
+
224
+ // Build TOML object
225
+ const obj = {};
226
+
227
+ // name — required
228
+ obj.name = frontmatter.name || agentName;
229
+
230
+ // description — required
231
+ obj.description = frontmatter.description || '';
232
+
233
+ // developer_instructions — rewrite kit paths, then R2 escape
234
+ const rewrittenBody = rewriteKitPaths(body);
235
+ const escapedBody = escapeTripleQuotes(rewrittenBody);
236
+ obj.developer_instructions = escapedBody;
237
+
238
+ // model — optional
239
+ const codexModel = resolveModel(frontmatter.model, modelMap);
240
+ if (codexModel !== undefined) {
241
+ obj.model = codexModel;
242
+ }
243
+
244
+ // model_reasoning_effort — optional; effort field on agents is absent per kit policy
245
+ // (effort is on mk-* skills only). We still support it if present for future proofing.
246
+ if (frontmatter.effort) {
247
+ const effort = frontmatter.effort;
248
+ if (effort === 'high' || effort === 'medium') {
249
+ obj.model_reasoning_effort = effort;
250
+ }
251
+ // never emit xhigh (kit policy)
252
+ }
253
+
254
+ // skills.config — IAC-4 filtered
255
+ const rawSkills = Array.isArray(frontmatter.skills) ? frontmatter.skills : [];
256
+ const filteredSkills = filterSkills(rawSkills);
257
+ if (filteredSkills.length > 0) {
258
+ obj.skills = {
259
+ config: filteredSkills.map((s) => ({
260
+ path: `.codex/skills/${s}`,
261
+ enabled: true,
262
+ })),
263
+ };
264
+ }
265
+
266
+ // Emit TOML
267
+ const tomlStr = toml.stringify(obj);
268
+
269
+ // R2 round-trip assertion
270
+ let reparsed;
271
+ try {
272
+ reparsed = toml.parse(tomlStr);
273
+ } catch (err) {
274
+ throw new Error(
275
+ `[R2] TOML round-trip parse failed for agent "${agentName}": ${err.message}`
276
+ );
277
+ }
278
+ if (reparsed.developer_instructions !== escapedBody) {
279
+ throw new Error(
280
+ `[R2] developer_instructions did not survive round-trip for agent "${agentName}". ` +
281
+ `Original length: ${escapedBody.length}, reparsed length: ${reparsed.developer_instructions?.length}`
282
+ );
283
+ }
284
+
285
+ return { name: agentName, tomlStr };
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Main
290
+ // ---------------------------------------------------------------------------
291
+
292
+ // Collect .md files (exclude README.md only; implementer.md is Tier-2 and IS converted)
293
+ const EXCLUDED_AGENTS = new Set(['README']);
294
+
295
+ let mdFiles;
296
+ try {
297
+ mdFiles = readdirSync(agentsDir)
298
+ .filter((f) => f.endsWith('.md') && !EXCLUDED_AGENTS.has(basename(f, '.md')))
299
+ .map((f) => join(agentsDir, f));
300
+ } catch (err) {
301
+ process.stderr.write(
302
+ `[convert-agents-to-codex] ERROR: Cannot read agents directory: ${agentsDir}\n${err.message}\n`
303
+ );
304
+ process.exit(1);
305
+ }
306
+
307
+ if (mdFiles.length === 0) {
308
+ process.stderr.write(
309
+ `[convert-agents-to-codex] ERROR: No .md agent files found in ${agentsDir}\n`
310
+ );
311
+ process.exit(1);
312
+ }
313
+
314
+ // Ensure output directory exists
315
+ try {
316
+ mkdirSync(outputDir, { recursive: true });
317
+ } catch (err) {
318
+ process.stderr.write(
319
+ `[convert-agents-to-codex] ERROR: Cannot create output directory: ${outputDir}\n${err.message}\n`
320
+ );
321
+ process.exit(1);
322
+ }
323
+
324
+ let successCount = 0;
325
+ let errorCount = 0;
326
+ const errors = [];
327
+
328
+ for (const mdPath of mdFiles) {
329
+ const agentName = basename(mdPath, '.md');
330
+ try {
331
+ const { name, tomlStr } = convertAgent(mdPath);
332
+ const outPath = join(outputDir, `${name}.toml`);
333
+ writeFileSync(outPath, tomlStr, 'utf8');
334
+ successCount++;
335
+ } catch (err) {
336
+ errorCount++;
337
+ errors.push(` ${agentName}: ${err.message}`);
338
+ process.stderr.write(
339
+ `[convert-agents-to-codex] ERROR converting "${agentName}": ${err.message}\n`
340
+ );
341
+ }
342
+ }
343
+
344
+ process.stdout.write(
345
+ `[convert-agents-to-codex] Done. ${successCount} converted, ${errorCount} errors.\n`
346
+ );
347
+ process.stdout.write(`[convert-agents-to-codex] Output: ${outputDir}\n`);
348
+
349
+ if (errors.length > 0) {
350
+ process.stderr.write(`[convert-agents-to-codex] Errors:\n${errors.join('\n')}\n`);
351
+ process.exit(1);
352
+ }