@khanhcan148/mk 0.1.26 → 0.1.28
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 +1 -0
- package/package.json +2 -3
- package/scripts/convert-hooks-to-codex.js +187 -32
- package/scripts/convert-skills-to-codex.js +115 -1
- package/src/commands/codex.js +25 -2
- package/src/lib/toml-emit.js +1 -1
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.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "CLI to install and manage MyClaudeKit (.claude/) in your projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,10 +26,9 @@
|
|
|
26
26
|
"chalk": "^5.0.0",
|
|
27
27
|
"commander": "^12.0.0",
|
|
28
28
|
"fs-extra": "^11.0.0",
|
|
29
|
-
"js-yaml": "4.1.
|
|
29
|
+
"js-yaml": "^4.1.1",
|
|
30
30
|
"semver": "^7.0.0"
|
|
31
31
|
},
|
|
32
|
-
"devDependencies": {},
|
|
33
32
|
"keywords": [
|
|
34
33
|
"claude",
|
|
35
34
|
"claude-code",
|
|
@@ -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
|
|
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
|
|
14
|
-
* │ PreToolUse
|
|
15
|
-
* │
|
|
16
|
-
* │ PostToolUse
|
|
17
|
-
* │
|
|
18
|
-
* │
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '^(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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(
|
|
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
290
|
emitTable('features', { codex_hooks: true }),
|
|
137
291
|
'',
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 };
|
|
@@ -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
|
-
|
|
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
|
}
|
package/src/commands/codex.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - .claude/skills/ → .codex/skills/ (via skills converter, Claude-only FM keys stripped)
|
|
8
8
|
* - .claude/workflows/ → .codex/workflows/ (recursive copy)
|
|
9
9
|
* - .claude/hooks/ → .codex/hooks/ (via hooks converter)
|
|
10
|
-
* - .codex/config.toml — emitted with [features]
|
|
10
|
+
* - .codex/config.toml — emitted with [features] hooks=true + 6 hook blocks
|
|
11
11
|
*
|
|
12
12
|
* The converter is the source of truth for IAC-1/3/4 + R2 compliance; this
|
|
13
13
|
* command is a thin wrapper that supplies cwd-derived paths and handles the
|
|
@@ -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.
|
package/src/lib/toml-emit.js
CHANGED
|
@@ -170,7 +170,7 @@ export function emitArrayOfTables(key, arr) {
|
|
|
170
170
|
* ```js
|
|
171
171
|
* {
|
|
172
172
|
* __leadingComments?: string, // file-level comments, emitted first
|
|
173
|
-
* features?: {
|
|
173
|
+
* features?: { hooks: boolean, ... }, // [features] table, emitted first among sections
|
|
174
174
|
* 'hooks.PreToolUse'?: { __type: 'array', items: [...] }, // array-of-tables
|
|
175
175
|
* 'hooks.PostToolUse'?: { __type: 'array', items: [...] },
|
|
176
176
|
* 'hooks.SessionStart'?: { __type: 'array', items: [...] },
|