@khanhcan148/mk 0.1.20 → 0.1.22
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 +9 -3
- package/bin/mk.js +8 -3
- package/package.json +11 -4
- package/scripts/.gitkeep +0 -0
- package/scripts/codex-diff-check.js +69 -0
- package/scripts/convert-agents-to-codex.js +352 -0
- package/scripts/convert-hooks-to-codex.js +204 -0
- package/scripts/convert-skills-to-codex.js +347 -0
- package/src/commands/codex.js +315 -0
- package/src/commands/init.js +0 -1
- package/src/commands/update.js +107 -3
- package/src/lib/codex-rewrite.js +36 -0
- package/src/lib/constants.js +26 -0
- package/src/lib/runtime-codex.js +148 -0
- package/src/lib/toml-emit.js +219 -0
- package/src/commands/vault.js +0 -363
- package/src/lib/gitignore-helper.js +0 -110
- package/src/lib/vault-binding.js +0 -205
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-codex.js — MODEL_MAP and model resolution helpers for the Codex converter.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* MODEL_MAP — frozen default map of Claude model tiers → Codex model IDs
|
|
6
|
+
* loadModelMap() — merge user override TOML over the frozen default
|
|
7
|
+
* resolveModel() — look up a Claude model name in a model map; warn on miss
|
|
8
|
+
*
|
|
9
|
+
* Design notes
|
|
10
|
+
* ------------
|
|
11
|
+
* K3=3a: hardcoded MODEL_MAP with per-invocation --model-map override flag.
|
|
12
|
+
* The default map is intentionally small and frozen; future model IDs arrive
|
|
13
|
+
* via the override TOML, not via source edits.
|
|
14
|
+
*
|
|
15
|
+
* ARP-02: unknown model names produce a console.warn (non-fatal) and return
|
|
16
|
+
* undefined so the caller may omit the `model` field from the emitted TOML.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimal TOML parser for the model-map override format.
|
|
23
|
+
* Supports only [section] headers and `key = "string"` entries.
|
|
24
|
+
* Throws on unrecognised syntax.
|
|
25
|
+
* @param {string} raw
|
|
26
|
+
* @returns {{ [section: string]: { [key: string]: string } }}
|
|
27
|
+
*/
|
|
28
|
+
function parseModelMapToml(raw) {
|
|
29
|
+
const result = {};
|
|
30
|
+
let section = null;
|
|
31
|
+
for (const rawLine of raw.split('\n')) {
|
|
32
|
+
const line = rawLine.replace(/\r$/, '').trim();
|
|
33
|
+
if (!line || line.startsWith('#')) continue;
|
|
34
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
35
|
+
if (sectionMatch) {
|
|
36
|
+
section = sectionMatch[1].trim();
|
|
37
|
+
result[section] = {};
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const kvMatch = line.match(/^([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"$/);
|
|
41
|
+
if (kvMatch && section) {
|
|
42
|
+
result[section][kvMatch[1]] = kvMatch[2];
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Unrecognised TOML line: ${line}`);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default mapping from Claude model-tier short names to Codex model IDs.
|
|
52
|
+
* Keys are the values that appear in agent YAML frontmatter `model:` fields.
|
|
53
|
+
* @type {Readonly<{[claudeModel: string]: string}>}
|
|
54
|
+
*/
|
|
55
|
+
export const MODEL_MAP = Object.freeze({
|
|
56
|
+
opus: 'gpt-5',
|
|
57
|
+
sonnet: 'gpt-5-mini',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load a model map, optionally merging a user-supplied TOML override.
|
|
62
|
+
*
|
|
63
|
+
* @param {string|undefined} overridePath Absolute path to a TOML file that
|
|
64
|
+
* contains a `[model_map]` table, e.g.:
|
|
65
|
+
* ```toml
|
|
66
|
+
* [model_map]
|
|
67
|
+
* opus = "gpt-5"
|
|
68
|
+
* sonnet = "gpt-5-mini"
|
|
69
|
+
* haiku = "gpt-4o-mini"
|
|
70
|
+
* ```
|
|
71
|
+
* When `undefined`, returns the frozen default MODEL_MAP.
|
|
72
|
+
* @returns {Readonly<{[claudeModel: string]: string}>}
|
|
73
|
+
* @throws {Error} If `overridePath` is provided but the file cannot be read
|
|
74
|
+
* or parsed, or if it does not contain a `[model_map]` table.
|
|
75
|
+
*/
|
|
76
|
+
export function loadModelMap(overridePath) {
|
|
77
|
+
if (overridePath === undefined) {
|
|
78
|
+
return MODEL_MAP;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = readFileSync(overridePath, 'utf8');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[runtime-codex] Cannot read model-map override file: ${overridePath}\n${err.message}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = parseModelMapToml(raw);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[runtime-codex] Failed to parse model-map override TOML: ${overridePath}\n${err.message}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!parsed.model_map || typeof parsed.model_map !== 'object') {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[runtime-codex] Override TOML must contain a [model_map] table: ${overridePath}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// H3: Validate model-map values to prevent TOML/YAML injection via crafted
|
|
106
|
+
// model names that contain whitespace, colons, or newlines.
|
|
107
|
+
const MODEL_VALUE_RE = /^[A-Za-z0-9._-]+$/;
|
|
108
|
+
for (const [k, v] of Object.entries(parsed.model_map)) {
|
|
109
|
+
if (!MODEL_VALUE_RE.test(String(v))) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`[runtime-codex] Invalid model-map value for "${k}": "${v}". ` +
|
|
112
|
+
'Values must match /^[A-Za-z0-9._-]+$/ (no whitespace, colons, or newlines).'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Object.freeze({ ...MODEL_MAP, ...parsed.model_map });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a Claude model-tier name to a Codex model ID.
|
|
122
|
+
*
|
|
123
|
+
* @param {string|undefined} claudeModel Value from agent frontmatter `model:` field.
|
|
124
|
+
* May be undefined (agent has no `model:` declaration).
|
|
125
|
+
* @param {Readonly<{[claudeModel: string]: string}>} modelMap Map returned by
|
|
126
|
+
* `loadModelMap()`.
|
|
127
|
+
* @returns {string|undefined} Codex model ID, or `undefined` when:
|
|
128
|
+
* - `claudeModel` is undefined (caller should omit the `model` field)
|
|
129
|
+
* - `claudeModel` is not in the map (ARP-02: warn emitted to stderr)
|
|
130
|
+
*/
|
|
131
|
+
export function resolveModel(claudeModel, modelMap) {
|
|
132
|
+
if (claudeModel === undefined) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const codexModel = modelMap[claudeModel];
|
|
137
|
+
if (codexModel === undefined) {
|
|
138
|
+
// ARP-02: unknown model — non-fatal, let caller omit the field
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
`[runtime-codex] Warning: unknown model tier "${claudeModel}" — ` +
|
|
141
|
+
`model field will be omitted from emitted TOML. ` +
|
|
142
|
+
`Add an entry to your --model-map override file to suppress this warning.\n`
|
|
143
|
+
);
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return codexModel;
|
|
148
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toml-emit.js — Inline TOML serialiser (no @iarna/toml runtime dependency).
|
|
3
|
+
*
|
|
4
|
+
* Scope: top-level table + array-of-tables ([[key]]) + string + boolean + number.
|
|
5
|
+
* Does NOT support inline arrays of mixed types or nested tables beyond
|
|
6
|
+
* array-of-tables — throws a TypeError for unsupported shapes.
|
|
7
|
+
*
|
|
8
|
+
* Hard constraints met:
|
|
9
|
+
* HC #2: no @iarna/toml runtime import.
|
|
10
|
+
* HC #4: in-table key order = matcher first, command second, remaining lex-sorted.
|
|
11
|
+
* Array-of-tables blocks sorted by (matcher, command) lex.
|
|
12
|
+
* Always uses Array.from(...).sort() — never raw Object.keys iteration.
|
|
13
|
+
*
|
|
14
|
+
* @iarna/toml remains a devDep used ONLY in test round-trip assertions.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escape a raw string value to a TOML basic-string literal (without wrapping quotes).
|
|
19
|
+
* Handles TOML 1.0 required escapes: \\ \" \b \f \n \r \t \uXXXX for control chars.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} s
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function escapeString(s) {
|
|
25
|
+
let out = '';
|
|
26
|
+
for (let i = 0; i < s.length; i++) {
|
|
27
|
+
const ch = s[i];
|
|
28
|
+
const cp = s.charCodeAt(i);
|
|
29
|
+
if (ch === '\\') { out += '\\\\'; }
|
|
30
|
+
else if (ch === '"') { out += '\\"'; }
|
|
31
|
+
else if (ch === '\b') { out += '\\b'; }
|
|
32
|
+
else if (ch === '\f') { out += '\\f'; }
|
|
33
|
+
else if (ch === '\n') { out += '\\n'; }
|
|
34
|
+
else if (ch === '\r') { out += '\\r'; }
|
|
35
|
+
else if (ch === '\t') { out += '\\t'; }
|
|
36
|
+
else if (cp < 0x20 || cp === 0x7f) {
|
|
37
|
+
out += '\\u' + cp.toString(16).padStart(4, '0');
|
|
38
|
+
} else {
|
|
39
|
+
out += ch;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Emit a scalar value as a TOML RHS token.
|
|
47
|
+
*
|
|
48
|
+
* @param {string|boolean|number|null|undefined} val
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function emitScalar(val) {
|
|
52
|
+
if (val === null || val === undefined) {
|
|
53
|
+
throw new TypeError(`[toml-emit] null/undefined is not a valid TOML scalar`);
|
|
54
|
+
}
|
|
55
|
+
if (typeof val === 'boolean') return val ? 'true' : 'false';
|
|
56
|
+
if (typeof val === 'number') return String(val);
|
|
57
|
+
if (typeof val === 'string') return `"${escapeString(val)}"`;
|
|
58
|
+
throw new TypeError(`[toml-emit] Unsupported scalar type: ${typeof val} (${String(val)})`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sort keys in the canonical order:
|
|
63
|
+
* 1. matcher (first)
|
|
64
|
+
* 2. command (second)
|
|
65
|
+
* 3. all other keys, lexicographically sorted
|
|
66
|
+
*
|
|
67
|
+
* @param {string[]} keys
|
|
68
|
+
* @returns {string[]}
|
|
69
|
+
*/
|
|
70
|
+
function sortKeys(keys) {
|
|
71
|
+
const filtered = Array.from(keys).filter(k => k !== '__leadingComments');
|
|
72
|
+
const priority = ['matcher', 'command'];
|
|
73
|
+
const fixed = priority.filter(k => filtered.includes(k));
|
|
74
|
+
const rest = Array.from(filtered)
|
|
75
|
+
.filter(k => !priority.includes(k))
|
|
76
|
+
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
77
|
+
return [...fixed, ...rest];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Emit a TOML table section: `[key]` header + key=value lines.
|
|
82
|
+
*
|
|
83
|
+
* Respects `__leadingComments` synthetic key: if present, emits those comment
|
|
84
|
+
* lines (verbatim) before the section header; the key itself is NOT emitted.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} key Section header name (e.g. "features", "hooks.PreToolUse")
|
|
87
|
+
* @param {object} obj Key-value pairs; values must be scalars.
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
export function emitTable(key, obj) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
|
|
93
|
+
if (obj.__leadingComments) {
|
|
94
|
+
for (const line of obj.__leadingComments.split('\n')) {
|
|
95
|
+
lines.push(line);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push(`[${key}]`);
|
|
100
|
+
|
|
101
|
+
for (const k of sortKeys(Object.keys(obj))) {
|
|
102
|
+
const v = obj[k];
|
|
103
|
+
if (typeof v === 'object' && v !== null) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`[toml-emit] emitTable does not support nested objects. Key "${k}" in table "${key}" ` +
|
|
106
|
+
`has type object. Use emitArrayOfTables for array-of-tables sections.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
lines.push(`${k} = ${emitScalar(v)}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lines.join('\n') + '\n';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Emit an array-of-tables section: multiple `[[key]]` blocks.
|
|
117
|
+
*
|
|
118
|
+
* Each block is sorted by `(matcher, command)` lex before emission.
|
|
119
|
+
* Within each block, key order follows the canonical sort: matcher, command, rest lex.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} key Section header name (e.g. "hooks.PreToolUse")
|
|
122
|
+
* @param {object[]} arr Array of objects; each object is one [[key]] block.
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
export function emitArrayOfTables(key, arr) {
|
|
126
|
+
// Sort by (matcher, command) — both fields optional but present in hooks usage
|
|
127
|
+
const sorted = Array.from(arr).sort((a, b) => {
|
|
128
|
+
const am = String(a.matcher ?? '');
|
|
129
|
+
const bm = String(b.matcher ?? '');
|
|
130
|
+
if (am !== bm) return am < bm ? -1 : 1;
|
|
131
|
+
const ac = String(a.command ?? '');
|
|
132
|
+
const bc = String(b.command ?? '');
|
|
133
|
+
return ac < bc ? -1 : ac > bc ? 1 : 0;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const chunks = [];
|
|
137
|
+
for (const block of sorted) {
|
|
138
|
+
const lines = [];
|
|
139
|
+
|
|
140
|
+
// Block-level leading comment
|
|
141
|
+
if (block.__leadingComments) {
|
|
142
|
+
for (const line of block.__leadingComments.split('\n')) {
|
|
143
|
+
lines.push(line);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines.push(`[[${key}]]`);
|
|
148
|
+
|
|
149
|
+
for (const k of sortKeys(Object.keys(block))) {
|
|
150
|
+
const v = block[k];
|
|
151
|
+
if (typeof v === 'object' && v !== null) {
|
|
152
|
+
throw new TypeError(
|
|
153
|
+
`[toml-emit] emitArrayOfTables does not support nested objects. ` +
|
|
154
|
+
`Key "${k}" in [[${key}]] has type object.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
lines.push(`${k} = ${emitScalar(v)}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
chunks.push(lines.join('\n'));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return chunks.join('\n') + '\n';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Emit a complete TOML document from a structured descriptor object.
|
|
168
|
+
*
|
|
169
|
+
* Document descriptor shape:
|
|
170
|
+
* ```js
|
|
171
|
+
* {
|
|
172
|
+
* __leadingComments?: string, // file-level comments, emitted first
|
|
173
|
+
* features?: { codex_hooks: boolean, ... }, // [features] table, emitted first among sections
|
|
174
|
+
* 'hooks.PreToolUse'?: { __type: 'array', items: [...] }, // array-of-tables
|
|
175
|
+
* 'hooks.PostToolUse'?: { __type: 'array', items: [...] },
|
|
176
|
+
* 'hooks.SessionStart'?: { __type: 'array', items: [...] },
|
|
177
|
+
* // other plain table keys
|
|
178
|
+
* }
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* Emission order:
|
|
182
|
+
* 1. File-level `__leadingComments`
|
|
183
|
+
* 2. `[features]` block (if present)
|
|
184
|
+
* 3. All other sections in insertion order (caller controls order)
|
|
185
|
+
*
|
|
186
|
+
* @param {object} doc
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
export function emitToml(doc) {
|
|
190
|
+
const parts = [];
|
|
191
|
+
|
|
192
|
+
// File-level leading comments
|
|
193
|
+
if (doc.__leadingComments) {
|
|
194
|
+
parts.push(doc.__leadingComments + '\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// [features] always first among sections
|
|
198
|
+
if (doc.features) {
|
|
199
|
+
parts.push(emitTable('features', doc.features));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Remaining sections in iteration order (caller sorts)
|
|
203
|
+
for (const key of Object.keys(doc)) {
|
|
204
|
+
if (key === '__leadingComments' || key === 'features') continue;
|
|
205
|
+
|
|
206
|
+
const val = doc[key];
|
|
207
|
+
if (val && typeof val === 'object' && val.__type === 'array') {
|
|
208
|
+
parts.push(emitArrayOfTables(key, val.items));
|
|
209
|
+
} else if (val && typeof val === 'object') {
|
|
210
|
+
parts.push(emitTable(key, val));
|
|
211
|
+
} else {
|
|
212
|
+
throw new TypeError(
|
|
213
|
+
`[toml-emit] Unsupported document-level value for key "${key}": expected object or array-descriptor`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return parts.join('\n');
|
|
219
|
+
}
|