@khanhcan148/mk 0.1.27 → 0.1.29

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/bin/mk.js CHANGED
@@ -37,6 +37,7 @@ program.command('remove')
37
37
 
38
38
  program.command('codex')
39
39
  .description('Mirror .claude/ to .codex/ for OpenAI Codex CLI (agents converted to TOML; skills + workflows copied)')
40
+ .option('--global', 'Convert ~/.claude/ to ~/.codex/')
40
41
  .option('--cwd <dir>', 'Project directory (default: current working directory)')
41
42
  .option('--output <dir>', 'Output directory (default: <cwd>/.codex/agents)')
42
43
  .option('--model-map <toml>', 'Path to a TOML file with [model_map] overrides')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanhcan148/mk",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "CLI to install and manage MyClaudeKit (.claude/) in your projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,18 +4,19 @@
4
4
  *
5
5
  * Reads `.claude/settings.json` → extracts hooks array.
6
6
  * Copies `.claude/hooks/` (including lib/) to `.codex/hooks/`.
7
- * Emits a Codex-compatible TOML config fragment with 6 hook blocks.
7
+ * Emits a Codex-compatible TOML config with 7 command hook handlers.
8
8
  *
9
9
  * Hook mapping (Claude → Codex):
10
10
  * ┌──────────────────────────────┬──────────────┬─────────────────────┬──────────────────────────────────────────────┐
11
11
  * │ Claude hook │ Codex event │ matcher │ command │
12
12
  * ├──────────────────────────────┼──────────────┼─────────────────────┼──────────────────────────────────────────────┤
13
- * │ PreToolUse Edit/Write │ PreToolUse │ ^(apply_patch)$ │ …/codex-payload-adapter.cjs privacy-block.cjs│
14
- * │ PreToolUse Bash │ PreToolUse │ ^(Bash)$ │ …/codex-payload-adapter.cjs credential-scan
15
- * │ PostToolUse Edit/Write PostToolUse │ ^(apply_patch)$ │ …/codex-payload-adapter.cjs comment-replace
16
- * │ PostToolUse test-match Bash │ PostToolUse │ ^(Bash)$ │ …/codex-payload-adapter.cjs test-match.cjs
17
- * │ SessionStart SessionStart clear node ./.codex/hooks/session-init.cjs
18
- * │ wiki-update-reminder PostToolUse ^$ │ node ./.codex/hooks/wiki-update-reminder.cjs
13
+ * │ PreToolUse apply_patch │ PreToolUse │ ^(apply_patch)$ │ …/codex-payload-adapter.cjs privacy-block.cjs
14
+ * │ PreToolUse apply_patch │ PreToolUse │ ^(apply_patch)$ │ …/codex-payload-adapter.cjs credential-scan
15
+ * │ PreToolUse Bash PreToolUse │ ^(Bash)$ │ …/codex-payload-adapter.cjs privacy-block.cjs
16
+ * │ PostToolUse apply_patch │ PostToolUse │ ^(apply_patch)$ │ …/codex-payload-adapter.cjs comment-replace
17
+ * │ PostToolUse apply_patch PostToolUse ^(apply_patch)$ …/codex-payload-adapter.cjs test-match.cjs
18
+ * │ SessionStart SessionStart clear │ node ./.codex/hooks/session-init.cjs
19
+ * │ wiki-update-reminder │ Stop │ n/a │ node ./.codex/hooks/wiki-update-reminder.cjs │
19
20
  * └──────────────────────────────┴──────────────┴─────────────────────┴──────────────────────────────────────────────┘
20
21
  *
21
22
  * Dropped (emit stderr warning per dropped hook):
@@ -31,7 +32,7 @@
31
32
  import { existsSync, cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
32
33
  import { join, resolve, dirname, basename } from 'node:path';
33
34
  import { fileURLToPath } from 'node:url';
34
- import { emitToml, emitArrayOfTables, emitTable } from '../src/lib/toml-emit.js';
35
+ import { emitScalar, emitTable } from '../src/lib/toml-emit.js';
35
36
 
36
37
  const __filename = fileURLToPath(import.meta.url);
37
38
  const __dirname = dirname(__filename);
@@ -44,22 +45,66 @@ const DROPPED_HOOKS = [
44
45
  { source: 'TaskCompleted', name: 'task-completed-handler' },
45
46
  ];
46
47
 
48
+ function shellDoubleQuote(s) {
49
+ return String(s).replace(/(["\\$`])/g, '\\$1');
50
+ }
51
+
52
+ function rootCommand(command, projectRoot, hookPath) {
53
+ const fallbackRoot = shellDoubleQuote(projectRoot);
54
+ return [
55
+ 'r="$(git rev-parse --show-toplevel 2>/dev/null)"',
56
+ `if [ -n "$r" ] && [ -f "$r/.codex/hooks/${hookPath}" ]; then cd "$r"; else cd "${fallbackRoot}"; fi`,
57
+ command,
58
+ ].join('; ');
59
+ }
60
+
47
61
  /**
48
- * The 6 canonical Codex hook blocks to emit.
62
+ * The 7 canonical Codex hook blocks to emit.
49
63
  * Order is stable/deterministic (used for byte-identical output guarantee).
50
64
  */
51
- function buildHookBlocks() {
65
+ function buildHookBlocks(projectRoot = PACKAGE_ROOT) {
52
66
  return {
53
67
  'hooks.PreToolUse': {
54
68
  __type: 'array',
55
69
  items: [
56
70
  {
57
71
  matcher: '^(apply_patch)$',
58
- command: 'node ./.codex/hooks/lib/codex-payload-adapter.cjs privacy-block.cjs',
72
+ hooks: [
73
+ {
74
+ type: 'command',
75
+ command: rootCommand(
76
+ 'node ./.codex/hooks/lib/codex-payload-adapter.cjs privacy-block.cjs',
77
+ projectRoot,
78
+ 'lib/codex-payload-adapter.cjs'
79
+ ),
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ matcher: '^(apply_patch)$',
85
+ hooks: [
86
+ {
87
+ type: 'command',
88
+ command: rootCommand(
89
+ 'node ./.codex/hooks/lib/codex-payload-adapter.cjs credential-scan.cjs',
90
+ projectRoot,
91
+ 'lib/codex-payload-adapter.cjs'
92
+ ),
93
+ },
94
+ ],
59
95
  },
60
96
  {
61
97
  matcher: '^(Bash)$',
62
- command: 'node ./.codex/hooks/lib/codex-payload-adapter.cjs credential-scan.cjs',
98
+ hooks: [
99
+ {
100
+ type: 'command',
101
+ command: rootCommand(
102
+ 'node ./.codex/hooks/lib/codex-payload-adapter.cjs privacy-block.cjs',
103
+ projectRoot,
104
+ 'lib/codex-payload-adapter.cjs'
105
+ ),
106
+ },
107
+ ],
63
108
  },
64
109
  ],
65
110
  },
@@ -68,16 +113,29 @@ function buildHookBlocks() {
68
113
  items: [
69
114
  {
70
115
  matcher: '^(apply_patch)$',
71
- command: 'node ./.codex/hooks/lib/codex-payload-adapter.cjs comment-replacement.cjs',
116
+ hooks: [
117
+ {
118
+ type: 'command',
119
+ command: rootCommand(
120
+ 'node ./.codex/hooks/lib/codex-payload-adapter.cjs comment-replacement.cjs',
121
+ projectRoot,
122
+ 'lib/codex-payload-adapter.cjs'
123
+ ),
124
+ },
125
+ ],
72
126
  },
73
127
  {
74
- matcher: '^(Bash)$',
75
- command: 'node ./.codex/hooks/lib/codex-payload-adapter.cjs test-match.cjs',
76
- },
77
- {
78
- __leadingComments: '# coverage-gap: matcher never fires in Codex (no AskUserQuestion equivalent)',
79
- matcher: '^$',
80
- command: 'node ./.codex/hooks/wiki-update-reminder.cjs',
128
+ matcher: '^(apply_patch)$',
129
+ hooks: [
130
+ {
131
+ type: 'command',
132
+ command: rootCommand(
133
+ 'node ./.codex/hooks/lib/codex-payload-adapter.cjs test-match.cjs',
134
+ projectRoot,
135
+ 'lib/codex-payload-adapter.cjs'
136
+ ),
137
+ },
138
+ ],
81
139
  },
82
140
  ],
83
141
  },
@@ -86,20 +144,116 @@ function buildHookBlocks() {
86
144
  items: [
87
145
  {
88
146
  matcher: 'clear',
89
- command: 'node ./.codex/hooks/session-init.cjs',
147
+ hooks: [
148
+ {
149
+ type: 'command',
150
+ command: rootCommand(
151
+ 'node ./.codex/hooks/session-init.cjs',
152
+ projectRoot,
153
+ 'session-init.cjs'
154
+ ),
155
+ },
156
+ ],
157
+ },
158
+ ],
159
+ },
160
+ 'hooks.Stop': {
161
+ __type: 'array',
162
+ items: [
163
+ {
164
+ hooks: [
165
+ {
166
+ type: 'command',
167
+ command: rootCommand(
168
+ 'node ./.codex/hooks/wiki-update-reminder.cjs',
169
+ projectRoot,
170
+ 'wiki-update-reminder.cjs'
171
+ ),
172
+ statusMessage: 'Checking wiki update reminder',
173
+ timeout: 10,
174
+ },
175
+ ],
90
176
  },
91
177
  ],
92
178
  },
93
179
  };
94
180
  }
95
181
 
182
+ function scalarKeys(obj) {
183
+ return Object.keys(obj)
184
+ .filter(k => k !== 'hooks' && k !== '__leadingComments')
185
+ .sort((a, b) => {
186
+ const priority = ['matcher'];
187
+ const ai = priority.includes(a) ? priority.indexOf(a) : priority.length;
188
+ const bi = priority.includes(b) ? priority.indexOf(b) : priority.length;
189
+ if (ai !== bi) return ai - bi;
190
+ return a < b ? -1 : a > b ? 1 : 0;
191
+ });
192
+ }
193
+
194
+ function handlerKeys(obj) {
195
+ return Object.keys(obj)
196
+ .filter(k => k !== '__leadingComments')
197
+ .sort((a, b) => {
198
+ const priority = ['command', 'type'];
199
+ const ai = priority.includes(a) ? priority.indexOf(a) : priority.length;
200
+ const bi = priority.includes(b) ? priority.indexOf(b) : priority.length;
201
+ if (ai !== bi) return ai - bi;
202
+ return a < b ? -1 : a > b ? 1 : 0;
203
+ });
204
+ }
205
+
206
+ function sortHookItems(items) {
207
+ return Array.from(items).sort((a, b) => {
208
+ const am = String(a.matcher ?? '');
209
+ const bm = String(b.matcher ?? '');
210
+ if (am !== bm) return am < bm ? -1 : 1;
211
+ const ac = String(a.hooks?.[0]?.command ?? '');
212
+ const bc = String(b.hooks?.[0]?.command ?? '');
213
+ return ac < bc ? -1 : ac > bc ? 1 : 0;
214
+ });
215
+ }
216
+
217
+ function emitHookGroups(key, items) {
218
+ const chunks = [];
219
+ for (const group of sortHookItems(items)) {
220
+ const lines = [];
221
+
222
+ if (group.__leadingComments) {
223
+ for (const line of group.__leadingComments.split('\n')) {
224
+ lines.push(line);
225
+ }
226
+ }
227
+
228
+ lines.push(`[[${key}]]`);
229
+ for (const scalarKey of scalarKeys(group)) {
230
+ lines.push(`${scalarKey} = ${emitScalar(group[scalarKey])}`);
231
+ }
232
+
233
+ for (const hook of group.hooks || []) {
234
+ if (hook.__leadingComments) {
235
+ for (const line of hook.__leadingComments.split('\n')) {
236
+ lines.push(line);
237
+ }
238
+ }
239
+ lines.push(`[[${key}.hooks]]`);
240
+ for (const hookKey of handlerKeys(hook)) {
241
+ lines.push(`${hookKey} = ${emitScalar(hook[hookKey])}`);
242
+ }
243
+ }
244
+
245
+ chunks.push(lines.join('\n'));
246
+ }
247
+ return chunks.join('\n') + '\n';
248
+ }
249
+
96
250
  /**
97
251
  * Copy `.claude/hooks/` tree to `.codex/hooks/`.
98
252
  * @param {string} projectRoot
99
253
  */
100
- function copyHooksTree(projectRoot) {
254
+ function copyHooksTree(projectRoot, outputDir) {
101
255
  const src = join(projectRoot, '.claude', 'hooks');
102
- const dest = join(projectRoot, '.codex', 'hooks');
256
+ const dest = join(outputDir, 'hooks');
103
257
 
104
258
  if (!existsSync(src)) {
105
259
  return; // Nothing to copy
@@ -127,17 +281,18 @@ function copyHooksTree(projectRoot) {
127
281
  * Build TOML document string for the Codex hooks config.
128
282
  * @returns {string}
129
283
  */
130
- function buildToml() {
131
- const hookBlocks = buildHookBlocks();
284
+ function buildToml(projectRoot = PACKAGE_ROOT) {
285
+ const hookBlocks = buildHookBlocks(projectRoot);
132
286
 
133
287
  const parts = [
134
288
  '# codex >=0.129.0 <0.140.0',
135
289
  '',
136
- emitTable('features', { hooks: true }),
290
+ emitTable('features', { codex_hooks: true }),
137
291
  '',
138
- emitArrayOfTables('hooks.PreToolUse', hookBlocks['hooks.PreToolUse'].items),
139
- emitArrayOfTables('hooks.PostToolUse', hookBlocks['hooks.PostToolUse'].items),
140
- emitArrayOfTables('hooks.SessionStart', hookBlocks['hooks.SessionStart'].items),
292
+ emitHookGroups('hooks.PreToolUse', hookBlocks['hooks.PreToolUse'].items),
293
+ emitHookGroups('hooks.PostToolUse', hookBlocks['hooks.PostToolUse'].items),
294
+ emitHookGroups('hooks.SessionStart', hookBlocks['hooks.SessionStart'].items),
295
+ emitHookGroups('hooks.Stop', hookBlocks['hooks.Stop'].items),
141
296
  ];
142
297
 
143
298
  return parts.join('\n');
@@ -172,20 +327,20 @@ export async function convertHooksToCodex(opts = {}) {
172
327
  }
173
328
 
174
329
  // Count emitted hooks
175
- const hookBlocks = buildHookBlocks();
330
+ const hookBlocks = buildHookBlocks(projectRoot);
176
331
  let added = 0;
177
332
  for (const section of Object.values(hookBlocks)) {
178
333
  if (section.__type === 'array') {
179
- added += section.items.length;
334
+ added += section.items.reduce((sum, item) => sum + (item.hooks || []).length, 0);
180
335
  }
181
336
  }
182
337
 
183
338
  // Copy hooks tree
184
- copyHooksTree(projectRoot);
339
+ copyHooksTree(projectRoot, outputDir);
185
340
 
186
341
  // Write config.toml
187
342
  mkdirSync(outputDir, { recursive: true });
188
- const tomlContent = buildToml();
343
+ const tomlContent = buildToml(projectRoot);
189
344
  writeFileSync(join(outputDir, 'config.toml'), tomlContent, 'utf-8');
190
345
 
191
346
  return { added, dropped, warnings };
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Compliance
16
16
  * ----------
17
- * SKL-1: model: value is mapped via MODEL_MAP (opus→gpt-5, sonnet→gpt-5-mini); effort and
17
+ * SKL-1: model: value is mapped via MODEL_MAP (opus→gpt-5.5, sonnet→gpt-5.4); effort and
18
18
  * argument-hint are stripped (no Codex skill-level equivalent).
19
19
  * SKL-2: Round-trip determinism — two consecutive runs produce byte-identical output
20
20
  * SKL-3: Body bytes pass through unchanged; only frontmatter is rewritten
@@ -147,6 +147,113 @@ function transformFrontmatter(content, map) {
147
147
  return `---\n${normalizedFm}---\n${body}`;
148
148
  }
149
149
 
150
+ // ---------------------------------------------------------------------------
151
+ // AskUserQuestion semantic rewrite for Codex
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * The gate section injected into every converted .md file that contains
156
+ * AskUserQuestion calls. Placed after the closing `---` frontmatter delimiter
157
+ * for SKILL.md files, or at the start of the file for reference files.
158
+ */
159
+ const CODEX_GATE_SECTION = `## Codex Interaction Gates
160
+ This skill was converted from Claude Code. Treat every \`AskUserQuestion(...)\` block below as a blocking Codex prompt gate:
161
+ - Render the question, header, and options in normal chat.
162
+ - Stop immediately after presenting the prompt.
163
+ - Wait for the user's next reply before continuing.
164
+ - Do not proceed to later phases until the answer is available.
165
+ - If running in a non-interactive or sub-agent context, use the fallback stated near the prompt.
166
+ `;
167
+
168
+ /**
169
+ * Rewrite AskUserQuestion prose phrases outside fenced blocks.
170
+ * Works line-by-line to track fence state, leaving content inside
171
+ * triple-backtick fences byte-for-byte unchanged.
172
+ *
173
+ * Rewrites applied outside fences:
174
+ * "Use AskUserQuestion to" → "Use a Codex prompt gate to"
175
+ * "If AskUserQuestion unavailable" → "If running non-interactively (Codex sub-agent context)"
176
+ *
177
+ * @param {string} content Raw Markdown content
178
+ * @returns {string} Rewritten content
179
+ */
180
+ function rewritePromptGatePhrases(content) {
181
+ const lines = content.split('\n');
182
+ let inFence = false;
183
+ const result = [];
184
+ for (const line of lines) {
185
+ // Detect fence boundaries: a line starting with ``` toggles fence state.
186
+ if (/^```/.test(line)) {
187
+ inFence = !inFence;
188
+ result.push(line);
189
+ continue;
190
+ }
191
+ if (inFence) {
192
+ result.push(line);
193
+ continue;
194
+ }
195
+ // Apply prose rewrites only outside fences.
196
+ let rewritten = line
197
+ .replace(/Use AskUserQuestion to/g, 'Use a Codex prompt gate to')
198
+ .replace(/If AskUserQuestion unavailable/g, 'If running non-interactively (Codex sub-agent context)');
199
+ result.push(rewritten);
200
+ }
201
+ return result.join('\n');
202
+ }
203
+
204
+ /**
205
+ * Inject the `## Codex Interaction Gates` section exactly once.
206
+ *
207
+ * Injection location:
208
+ * - Files with YAML frontmatter (starting with `---`): injected immediately
209
+ * after the closing `---\n` delimiter.
210
+ * - Files without frontmatter (reference files): injected at the start.
211
+ *
212
+ * Idempotent: if the section is already present, returns content unchanged.
213
+ *
214
+ * @param {string} content Raw Markdown content
215
+ * @returns {string} Content with gate section injected (exactly once)
216
+ */
217
+ function injectCodexGateSection(content) {
218
+ // Idempotency guard. Flat includes is safe here: '## Codex Interaction Gates' is only ever
219
+ // emitted by this function, never present in source .claude/skills/ files, so a fenced-block
220
+ // false-positive is impossible in practice.
221
+ if (content.includes('## Codex Interaction Gates')) {
222
+ return content;
223
+ }
224
+
225
+ // Has frontmatter: inject after closing `---`.
226
+ if (/^---\r?\n/.test(content)) {
227
+ // Find the position right after the closing `---` + newline.
228
+ const closingRe = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
229
+ const m = content.match(closingRe);
230
+ if (m) {
231
+ const insertAt = m[0].length;
232
+ return content.slice(0, insertAt) + CODEX_GATE_SECTION + '\n' + content.slice(insertAt);
233
+ }
234
+ }
235
+
236
+ // No frontmatter: inject at start.
237
+ return CODEX_GATE_SECTION + '\n' + content;
238
+ }
239
+
240
+ /**
241
+ * Full AskUserQuestion rewrite pipeline for a single Markdown file:
242
+ * 1. If the content contains no `AskUserQuestion`, return unchanged.
243
+ * 2. Inject the `## Codex Interaction Gates` section (idempotent).
244
+ * 3. Rewrite prose phrases outside fenced blocks.
245
+ *
246
+ * @param {string} content Raw Markdown content
247
+ * @returns {string} Transformed content
248
+ */
249
+ function rewriteAskUserQuestionForCodex(content) {
250
+ if (!content.includes('AskUserQuestion')) {
251
+ return content;
252
+ }
253
+ const withGate = injectCodexGateSection(content);
254
+ return rewritePromptGatePhrases(withGate);
255
+ }
256
+
150
257
  // ---------------------------------------------------------------------------
151
258
  // Post-copy rewrite walk
152
259
  // ---------------------------------------------------------------------------
@@ -159,6 +266,9 @@ function transformFrontmatter(content, map) {
159
266
  * Reads each file, applies `rewriteKitPaths`, and writes back only when the
160
267
  * content actually changes (avoids touching mtimes unnecessarily).
161
268
  *
269
+ * For `.md` files, also applies `rewriteAskUserQuestionForCodex` to inject
270
+ * Codex interaction gates and rewrite prose AskUserQuestion references.
271
+ *
162
272
  * @param {string} dir Absolute path to walk
163
273
  */
164
274
  const _REWRITE_EXTS = new Set(['.md', '.js', '.cjs', '.mjs', '.ts', '.py']);
@@ -177,7 +287,11 @@ function walkAndRewriteMarkdown(dir) {
177
287
  } else if (entry.isFile() && _REWRITE_EXTS.has(extname(entry.name).toLowerCase())) {
178
288
  try {
179
289
  const original = readFileSync(fullPath, 'utf8');
180
- const rewritten = rewriteKitPaths(original);
290
+ let rewritten = rewriteKitPaths(original);
291
+ // For Markdown files, additionally apply the AskUserQuestion rewrite.
292
+ if (extname(entry.name).toLowerCase() === '.md') {
293
+ rewritten = rewriteAskUserQuestionForCodex(rewritten);
294
+ }
181
295
  if (rewritten !== original) {
182
296
  writeFileSync(fullPath, rewritten, 'utf8');
183
297
  }
@@ -17,6 +17,7 @@
17
17
  import { spawn } from 'node:child_process';
18
18
  import { existsSync, cpSync, rmSync, statSync, writeFileSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
19
19
  import { join, resolve, dirname, basename, sep, parse as pathParse, extname } from 'node:path';
20
+ import { homedir } from 'node:os';
20
21
  import { fileURLToPath } from 'node:url';
21
22
  import chalk from 'chalk';
22
23
  import { TOOL_DIR_NAME, COPY_FILTER_PATTERNS } from '../lib/constants.js';
@@ -138,6 +139,20 @@ function mirrorDirWithRewrite(srcDir, destDir, opts = {}) {
138
139
  walkAndCopy(srcDir, destDir);
139
140
  }
140
141
 
142
+ /**
143
+ * Copy agent support references into `.codex/agents/references/` with the same
144
+ * path rewriting used for workflows. Agent TOML bodies reference these files at
145
+ * runtime, so the conversion is incomplete without them.
146
+ *
147
+ * @param {string} agentsDir
148
+ * @param {string} agentsOut
149
+ */
150
+ function mirrorAgentReferences(agentsDir, agentsOut) {
151
+ const referencesDir = join(agentsDir, 'references');
152
+ if (!isDirectory(referencesDir)) return;
153
+ mirrorDirWithRewrite(referencesDir, join(agentsOut, 'references'));
154
+ }
155
+
141
156
  /** Single-syscall directory check (replaces existsSync+statSync double-stat). */
142
157
  function isDirectory(p) {
143
158
  try { return statSync(p).isDirectory(); } catch { return false; }
@@ -170,6 +185,12 @@ function isDirectory(p) {
170
185
  */
171
186
  export async function runCodexConversion(options = {}) {
172
187
  const { verbose = true } = options;
188
+ if (options.global && options.cwd) {
189
+ const msg = 'error: --global cannot be combined with --cwd';
190
+ console.error(chalk.red(msg));
191
+ return { exitCode: 1, errors: [msg] };
192
+ }
193
+
173
194
  // S1: Reject `..` segments AFTER resolve to catch encoded traversal attempts
174
195
  // (e.g. "foo/../../../etc"). A full homedir-bound would break legitimate
175
196
  // `mk codex --cwd /tmp/…` invocations, so we use the lighter ..‑segment check.
@@ -186,7 +207,7 @@ export async function runCodexConversion(options = {}) {
186
207
  // (defence in depth: covers OS-level symlink chains that embed traversal)
187
208
  void resolvedCwd; // used only for the side-effect check above
188
209
  }
189
- const projectRoot = resolve(options.cwd || process.cwd());
210
+ const projectRoot = resolve(options.global ? homedir() : options.cwd || process.cwd());
190
211
  const claudeDir = join(projectRoot, TOOL_DIR_NAME);
191
212
  const agentsDir = join(claudeDir, 'agents');
192
213
  const skillsDir = join(claudeDir, 'skills');
@@ -300,6 +321,8 @@ export async function runCodexConversion(options = {}) {
300
321
  return { exitCode: code, errors };
301
322
  }
302
323
 
324
+ mirrorAgentReferences(agentsDir, agentsOut);
325
+
303
326
  // Mirror workflows synchronously after both converters succeed.
304
327
  // Uses mirrorDirWithRewrite so .claude/<subdir>/ refs in .md/.txt/.toml files
305
328
  // are rewritten to .codex/<subdir>/ for Codex-runtime resolution.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * codex-rewrite.js — Shared kit-path rewriter for Codex conversion pipeline.
3
3
  *
4
- * Rewrites kit-relative `.claude/<subdir>/` references to `.codex/<subdir>/`
4
+ * Rewrites kit-relative `.claude/<subdir>` references to `.codex/<subdir>`
5
5
  * so that Codex-runtime skill/workflow/agent prose resolves correctly under
6
6
  * `.codex/` rather than the source `.claude/` tree.
7
7
  *
@@ -23,14 +23,18 @@
23
23
  // Substring match is intentional: `~/.claude/skills/X` becomes
24
24
  // `~/.codex/skills/X`. No prefix anchor (handles backtick, quote, space,
25
25
  // start-of-string, and ~/ uniformly).
26
- const _KIT_PATH_RE = /\.claude\/(workflows|skills|agents|commands|hooks)\//g;
26
+ //
27
+ // The lookahead permits both directory refs (`.claude/skills`) and child refs
28
+ // (`.claude/skills/foo.md`) while blocking prefix false positives such as
29
+ // `.claude/skills-old`.
30
+ const _KIT_PATH_RE = /\.claude\/(workflows|skills|agents|commands|hooks)(?=\/|[`'"<>\s).,;:]|$)/g;
27
31
 
28
32
  /**
29
- * Rewrite `.claude/<subdir>/` references to `.codex/<subdir>/` in `text`.
33
+ * Rewrite `.claude/<subdir>` references to `.codex/<subdir>` in `text`.
30
34
  *
31
35
  * @param {string} text - Input text (may contain multiple refs across multiple lines).
32
36
  * @returns {string} - Text with all kit-internal path references rewritten.
33
37
  */
34
38
  export function rewriteKitPaths(text) {
35
- return text.replace(_KIT_PATH_RE, '.codex/$1/');
39
+ return text.replace(_KIT_PATH_RE, '.codex/$1');
36
40
  }
@@ -53,8 +53,8 @@ function parseModelMapToml(raw) {
53
53
  * @type {Readonly<{[claudeModel: string]: string}>}
54
54
  */
55
55
  export const MODEL_MAP = Object.freeze({
56
- opus: 'gpt-5',
57
- sonnet: 'gpt-5-mini',
56
+ opus: 'gpt-5.5',
57
+ sonnet: 'gpt-5.4',
58
58
  });
59
59
 
60
60
  /**
@@ -64,9 +64,9 @@ export const MODEL_MAP = Object.freeze({
64
64
  * contains a `[model_map]` table, e.g.:
65
65
  * ```toml
66
66
  * [model_map]
67
- * opus = "gpt-5"
68
- * sonnet = "gpt-5-mini"
69
- * haiku = "gpt-4o-mini"
67
+ * opus = "gpt-5.5"
68
+ * sonnet = "gpt-5.4"
69
+ * haiku = "gpt-5.4-mini"
70
70
  * ```
71
71
  * When `undefined`, returns the frozen default MODEL_MAP.
72
72
  * @returns {Readonly<{[claudeModel: string]: string}>}