@lesliechan721/aw-plugin-core 0.1.0-beta.1
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/dist/file-rules.d.ts +2 -0
- package/dist/file-rules.js +42 -0
- package/dist/file-rules.js.map +1 -0
- package/dist/generated/plugin-manifest-v1.d.ts +368 -0
- package/dist/generated/plugin-manifest-v1.js +3 -0
- package/dist/generated/plugin-manifest-v1.js.map +1 -0
- package/dist/hooks-contract.d.ts +189 -0
- package/dist/hooks-contract.js +967 -0
- package/dist/hooks-contract.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest-contract.d.ts +96 -0
- package/dist/manifest-contract.js +2730 -0
- package/dist/manifest-contract.js.map +1 -0
- package/dist/manifest-type-contract.d.ts +7 -0
- package/dist/manifest-type-contract.js +2 -0
- package/dist/manifest-type-contract.js.map +1 -0
- package/dist/manifest.d.ts +27 -0
- package/dist/manifest.js +59 -0
- package/dist/manifest.js.map +1 -0
- package/dist/permission-command.d.ts +3 -0
- package/dist/permission-command.js +11 -0
- package/dist/permission-command.js.map +1 -0
- package/dist/platform-entries.d.ts +13 -0
- package/dist/platform-entries.js +20 -0
- package/dist/platform-entries.js.map +1 -0
- package/dist/platform-support.d.ts +6 -0
- package/dist/platform-support.js +38 -0
- package/dist/platform-support.js.map +1 -0
- package/dist/plugin-release.d.ts +30 -0
- package/dist/plugin-release.js +187 -0
- package/dist/plugin-release.js.map +1 -0
- package/dist/plugin-script-contract.d.ts +33 -0
- package/dist/plugin-script-contract.js +599 -0
- package/dist/plugin-script-contract.js.map +1 -0
- package/dist/portable-skill-validation.d.ts +7 -0
- package/dist/portable-skill-validation.js +111 -0
- package/dist/portable-skill-validation.js.map +1 -0
- package/dist/read-json.d.ts +1 -0
- package/dist/read-json.js +5 -0
- package/dist/read-json.js.map +1 -0
- package/dist/runtime-export-source.d.ts +14 -0
- package/dist/runtime-export-source.js +73 -0
- package/dist/runtime-export-source.js.map +1 -0
- package/dist/skill-authoring.d.ts +2 -0
- package/dist/skill-authoring.js +13 -0
- package/dist/skill-authoring.js.map +1 -0
- package/dist/skill-frontmatter-capabilities.d.ts +36 -0
- package/dist/skill-frontmatter-capabilities.js +120 -0
- package/dist/skill-frontmatter-capabilities.js.map +1 -0
- package/dist/skill-frontmatter-capabilities.json +200 -0
- package/dist/slash-commands.d.ts +43 -0
- package/dist/slash-commands.js +394 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/template-engine.d.ts +2 -0
- package/dist/template-engine.js +26 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/types.d.ts +428 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +105 -0
- package/dist/workspace.js +963 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +46 -0
- package/schemas/plugin-manifest-v1.json +329 -0
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
import { resolvePlatformEntries } from './platform-entries.js';
|
|
2
|
+
export const PUBLIC_HOOK_EVENTS = [
|
|
3
|
+
'before_tool',
|
|
4
|
+
'after_tool',
|
|
5
|
+
'session_start',
|
|
6
|
+
];
|
|
7
|
+
export const PUBLIC_SESSION_START_SOURCES = [
|
|
8
|
+
'startup',
|
|
9
|
+
'resume',
|
|
10
|
+
'clear',
|
|
11
|
+
];
|
|
12
|
+
export const PUBLIC_SESSION_START_DEFAULT_MATCHER = PUBLIC_SESSION_START_SOURCES.join('|');
|
|
13
|
+
export const PUBLIC_HOOK_EVENT_RENDER_MAP = {
|
|
14
|
+
before_tool: {
|
|
15
|
+
claude: 'PreToolUse',
|
|
16
|
+
codex: 'PreToolUse',
|
|
17
|
+
},
|
|
18
|
+
after_tool: {
|
|
19
|
+
claude: 'PostToolUse',
|
|
20
|
+
codex: 'PostToolUse',
|
|
21
|
+
},
|
|
22
|
+
session_start: {
|
|
23
|
+
claude: 'SessionStart',
|
|
24
|
+
codex: 'SessionStart',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
// Claude allowlist source:
|
|
28
|
+
// - src/entrypoints/sdk/coreSchemas.ts `HOOK_EVENTS`
|
|
29
|
+
// - src/utils/hooks/hooksConfigManager.ts `HookEventMetadata.matcherMetadata`
|
|
30
|
+
// - src/utils/hooks.ts runtime `matchQuery`
|
|
31
|
+
// Verified against Claude Code source on 2026-04-27.
|
|
32
|
+
export const CLAUDE_NATIVE_HOOK_EVENTS = [
|
|
33
|
+
'PreToolUse',
|
|
34
|
+
'PostToolUse',
|
|
35
|
+
'PostToolUseFailure',
|
|
36
|
+
'Notification',
|
|
37
|
+
'UserPromptSubmit',
|
|
38
|
+
'SessionStart',
|
|
39
|
+
'SessionEnd',
|
|
40
|
+
'Stop',
|
|
41
|
+
'StopFailure',
|
|
42
|
+
'SubagentStart',
|
|
43
|
+
'SubagentStop',
|
|
44
|
+
'PreCompact',
|
|
45
|
+
'PostCompact',
|
|
46
|
+
'PermissionRequest',
|
|
47
|
+
'PermissionDenied',
|
|
48
|
+
'Setup',
|
|
49
|
+
'TeammateIdle',
|
|
50
|
+
'TaskCreated',
|
|
51
|
+
'TaskCompleted',
|
|
52
|
+
'Elicitation',
|
|
53
|
+
'ElicitationResult',
|
|
54
|
+
'ConfigChange',
|
|
55
|
+
'WorktreeCreate',
|
|
56
|
+
'WorktreeRemove',
|
|
57
|
+
'InstructionsLoaded',
|
|
58
|
+
'CwdChanged',
|
|
59
|
+
'FileChanged',
|
|
60
|
+
];
|
|
61
|
+
export const CODEX_NATIVE_HOOK_EVENTS = [
|
|
62
|
+
'PreToolUse',
|
|
63
|
+
'PermissionRequest',
|
|
64
|
+
'PostToolUse',
|
|
65
|
+
'SessionStart',
|
|
66
|
+
'UserPromptSubmit',
|
|
67
|
+
'Stop',
|
|
68
|
+
];
|
|
69
|
+
export const NATIVE_HOOK_EVENT_CONTRACTS = {
|
|
70
|
+
claude: {
|
|
71
|
+
PreToolUse: { matcherAllowed: true },
|
|
72
|
+
PostToolUse: { matcherAllowed: true },
|
|
73
|
+
PostToolUseFailure: { matcherAllowed: true },
|
|
74
|
+
Notification: { matcherAllowed: true },
|
|
75
|
+
UserPromptSubmit: { matcherAllowed: false },
|
|
76
|
+
SessionStart: { matcherAllowed: true },
|
|
77
|
+
SessionEnd: { matcherAllowed: true },
|
|
78
|
+
Stop: { matcherAllowed: false },
|
|
79
|
+
StopFailure: { matcherAllowed: true },
|
|
80
|
+
SubagentStart: { matcherAllowed: true },
|
|
81
|
+
SubagentStop: { matcherAllowed: true },
|
|
82
|
+
PreCompact: { matcherAllowed: true },
|
|
83
|
+
PostCompact: { matcherAllowed: true },
|
|
84
|
+
PermissionRequest: { matcherAllowed: true },
|
|
85
|
+
PermissionDenied: { matcherAllowed: true },
|
|
86
|
+
Setup: { matcherAllowed: true },
|
|
87
|
+
TeammateIdle: { matcherAllowed: false },
|
|
88
|
+
TaskCreated: { matcherAllowed: false },
|
|
89
|
+
TaskCompleted: { matcherAllowed: false },
|
|
90
|
+
Elicitation: { matcherAllowed: true },
|
|
91
|
+
ElicitationResult: { matcherAllowed: true },
|
|
92
|
+
ConfigChange: { matcherAllowed: true },
|
|
93
|
+
WorktreeCreate: { matcherAllowed: false },
|
|
94
|
+
WorktreeRemove: { matcherAllowed: false },
|
|
95
|
+
InstructionsLoaded: { matcherAllowed: true },
|
|
96
|
+
CwdChanged: { matcherAllowed: false },
|
|
97
|
+
// Claude runtime uses basename(file_path) as matchQuery for FileChanged.
|
|
98
|
+
FileChanged: { matcherAllowed: true },
|
|
99
|
+
},
|
|
100
|
+
codex: {
|
|
101
|
+
PreToolUse: { matcherAllowed: true },
|
|
102
|
+
PermissionRequest: { matcherAllowed: true },
|
|
103
|
+
PostToolUse: { matcherAllowed: true },
|
|
104
|
+
SessionStart: { matcherAllowed: true },
|
|
105
|
+
UserPromptSubmit: { matcherAllowed: false },
|
|
106
|
+
Stop: { matcherAllowed: false },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const PUBLIC_HOOK_EVENT_SET = new Set(PUBLIC_HOOK_EVENTS);
|
|
110
|
+
const PUBLIC_SESSION_START_SOURCE_SET = new Set(PUBLIC_SESSION_START_SOURCES);
|
|
111
|
+
const CODEX_EXACT_MATCHER_PATTERN = /^[A-Za-z0-9_|]+$/;
|
|
112
|
+
const CODEX_REGEX_FLAG_SET = new Set(['i', 'm', 'R', 's', 'U', 'u', 'x']);
|
|
113
|
+
const NATIVE_HOOK_EVENT_SETS = {
|
|
114
|
+
claude: new Set(CLAUDE_NATIVE_HOOK_EVENTS),
|
|
115
|
+
codex: new Set(CODEX_NATIVE_HOOK_EVENTS),
|
|
116
|
+
};
|
|
117
|
+
function assertNonEmptyString(value, fieldName) {
|
|
118
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
119
|
+
throw new Error(`Invalid ${fieldName}: expected a non-empty string.`);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function assertObject(value, fieldName) {
|
|
124
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
125
|
+
throw new Error(`Invalid ${fieldName}: expected an object.`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
function getNativeHookEventContract(event, platform, fieldName) {
|
|
130
|
+
const contracts = NATIVE_HOOK_EVENT_CONTRACTS[platform];
|
|
131
|
+
const contract = contracts[event];
|
|
132
|
+
if (!contract) {
|
|
133
|
+
throw new Error(`Invalid ${fieldName}: unsupported ${platform} hook event "${event}".`);
|
|
134
|
+
}
|
|
135
|
+
return contract;
|
|
136
|
+
}
|
|
137
|
+
function normalizePublicSessionStartMatcher(matcher, fieldName) {
|
|
138
|
+
const parts = matcher.split('|');
|
|
139
|
+
if (parts.length === 0) {
|
|
140
|
+
throw new Error(`Invalid ${fieldName}: expected startup/resume/clear sources joined by "|".`);
|
|
141
|
+
}
|
|
142
|
+
const selected = new Set();
|
|
143
|
+
for (const part of parts) {
|
|
144
|
+
if (!PUBLIC_SESSION_START_SOURCE_SET.has(part)) {
|
|
145
|
+
throw new Error(`Invalid ${fieldName}: public session_start only allows startup, resume, clear, or their "|" union.`);
|
|
146
|
+
}
|
|
147
|
+
selected.add(part);
|
|
148
|
+
}
|
|
149
|
+
return PUBLIC_SESSION_START_SOURCES.filter((part) => selected.has(part)).join('|');
|
|
150
|
+
}
|
|
151
|
+
function isCodexMatcherWhitespace(char) {
|
|
152
|
+
return char !== undefined && /\s/u.test(char);
|
|
153
|
+
}
|
|
154
|
+
function isCodexCaptureNameStart(char) {
|
|
155
|
+
return char !== undefined && /[A-Za-z_]/u.test(char);
|
|
156
|
+
}
|
|
157
|
+
function isCodexCaptureNameChar(char) {
|
|
158
|
+
return char !== undefined && /[A-Za-z0-9_.[\]]/u.test(char);
|
|
159
|
+
}
|
|
160
|
+
function createCodexMatcherSyntaxError(state, detail) {
|
|
161
|
+
return new Error(`${detail} at offset ${state.index}.`);
|
|
162
|
+
}
|
|
163
|
+
function currentCodexMatcherFlags(state) {
|
|
164
|
+
return state.flagStack[state.flagStack.length - 1];
|
|
165
|
+
}
|
|
166
|
+
function cloneCodexMatcherFlags(flags) {
|
|
167
|
+
return { ...flags };
|
|
168
|
+
}
|
|
169
|
+
function advanceCodexMatcherVerboseTrivia(state) {
|
|
170
|
+
if (!currentCodexMatcherFlags(state).x) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
while (state.index < state.source.length) {
|
|
174
|
+
const char = state.source[state.index];
|
|
175
|
+
if (isCodexMatcherWhitespace(char)) {
|
|
176
|
+
state.index += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (char === '#') {
|
|
180
|
+
state.index += 1;
|
|
181
|
+
while (state.index < state.source.length && state.source[state.index] !== '\n') {
|
|
182
|
+
state.index += 1;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function consumeCodexMatcherCaptureName(state) {
|
|
190
|
+
if (!isCodexCaptureNameStart(state.source[state.index])) {
|
|
191
|
+
throw createCodexMatcherSyntaxError(state, 'invalid capture group name');
|
|
192
|
+
}
|
|
193
|
+
state.index += 1;
|
|
194
|
+
while (isCodexCaptureNameChar(state.source[state.index])) {
|
|
195
|
+
state.index += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function consumeCodexMatcherHexDigits(state, length, detail) {
|
|
199
|
+
for (let count = 0; count < length; count += 1) {
|
|
200
|
+
const char = state.source[state.index];
|
|
201
|
+
if (char === undefined || !/[0-9A-Fa-f]/.test(char)) {
|
|
202
|
+
throw createCodexMatcherSyntaxError(state, detail);
|
|
203
|
+
}
|
|
204
|
+
state.index += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function consumeCodexMatcherBracedHexDigits(state, detail) {
|
|
208
|
+
if (state.source[state.index] !== '{') {
|
|
209
|
+
throw createCodexMatcherSyntaxError(state, detail);
|
|
210
|
+
}
|
|
211
|
+
state.index += 1;
|
|
212
|
+
let sawDigit = false;
|
|
213
|
+
while (state.index < state.source.length && state.source[state.index] !== '}') {
|
|
214
|
+
const char = state.source[state.index];
|
|
215
|
+
if (char === undefined || !/[0-9A-Fa-f_]/.test(char)) {
|
|
216
|
+
throw createCodexMatcherSyntaxError(state, detail);
|
|
217
|
+
}
|
|
218
|
+
if (char !== '_') {
|
|
219
|
+
sawDigit = true;
|
|
220
|
+
}
|
|
221
|
+
state.index += 1;
|
|
222
|
+
}
|
|
223
|
+
if (state.source[state.index] !== '}' || !sawDigit) {
|
|
224
|
+
throw createCodexMatcherSyntaxError(state, detail);
|
|
225
|
+
}
|
|
226
|
+
state.index += 1;
|
|
227
|
+
}
|
|
228
|
+
function consumeCodexMatcherUnicodeProperty(state) {
|
|
229
|
+
const char = state.source[state.index];
|
|
230
|
+
if (char === '{') {
|
|
231
|
+
state.index += 1;
|
|
232
|
+
let sawContent = false;
|
|
233
|
+
while (state.index < state.source.length && state.source[state.index] !== '}') {
|
|
234
|
+
sawContent = true;
|
|
235
|
+
state.index += 1;
|
|
236
|
+
}
|
|
237
|
+
if (state.source[state.index] !== '}' || !sawContent) {
|
|
238
|
+
throw createCodexMatcherSyntaxError(state, 'unterminated Unicode character class escape');
|
|
239
|
+
}
|
|
240
|
+
state.index += 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (char === undefined || !/[A-Za-z]/.test(char)) {
|
|
244
|
+
throw createCodexMatcherSyntaxError(state, 'invalid Unicode character class escape');
|
|
245
|
+
}
|
|
246
|
+
state.index += 1;
|
|
247
|
+
}
|
|
248
|
+
function consumeCodexMatcherEscape(state, inCharacterClass = false) {
|
|
249
|
+
state.index += 1;
|
|
250
|
+
const escaped = state.source[state.index];
|
|
251
|
+
if (escaped === undefined) {
|
|
252
|
+
throw createCodexMatcherSyntaxError(state, 'trailing escape');
|
|
253
|
+
}
|
|
254
|
+
if (/[0-9]/.test(escaped)) {
|
|
255
|
+
throw createCodexMatcherSyntaxError(state, 'backreferences and octal escapes are not supported by Codex matchers');
|
|
256
|
+
}
|
|
257
|
+
switch (escaped) {
|
|
258
|
+
case 'a':
|
|
259
|
+
case 'f':
|
|
260
|
+
case 't':
|
|
261
|
+
case 'n':
|
|
262
|
+
case 'r':
|
|
263
|
+
case 'v':
|
|
264
|
+
case 'd':
|
|
265
|
+
case 'D':
|
|
266
|
+
case 's':
|
|
267
|
+
case 'S':
|
|
268
|
+
case 'w':
|
|
269
|
+
case 'W':
|
|
270
|
+
state.index += 1;
|
|
271
|
+
return true;
|
|
272
|
+
case 'A':
|
|
273
|
+
case 'z':
|
|
274
|
+
case 'b':
|
|
275
|
+
case 'B':
|
|
276
|
+
case '<':
|
|
277
|
+
case '>':
|
|
278
|
+
if (inCharacterClass) {
|
|
279
|
+
throw createCodexMatcherSyntaxError(state, `escape \\${escaped} is not valid inside a character class`);
|
|
280
|
+
}
|
|
281
|
+
state.index += 1;
|
|
282
|
+
return false;
|
|
283
|
+
case 'x':
|
|
284
|
+
state.index += 1;
|
|
285
|
+
if (state.source[state.index] === '{') {
|
|
286
|
+
consumeCodexMatcherBracedHexDigits(state, 'invalid braced hex escape');
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
consumeCodexMatcherHexDigits(state, 2, 'invalid hex escape');
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
case 'u':
|
|
293
|
+
state.index += 1;
|
|
294
|
+
if (state.source[state.index] === '{') {
|
|
295
|
+
consumeCodexMatcherBracedHexDigits(state, 'invalid braced Unicode escape');
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
consumeCodexMatcherHexDigits(state, 4, 'invalid Unicode escape');
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
case 'U':
|
|
302
|
+
state.index += 1;
|
|
303
|
+
if (state.source[state.index] === '{') {
|
|
304
|
+
consumeCodexMatcherBracedHexDigits(state, 'invalid braced Unicode escape');
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
consumeCodexMatcherHexDigits(state, 8, 'invalid Unicode escape');
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
case 'p':
|
|
311
|
+
case 'P':
|
|
312
|
+
state.index += 1;
|
|
313
|
+
consumeCodexMatcherUnicodeProperty(state);
|
|
314
|
+
return true;
|
|
315
|
+
case 'k':
|
|
316
|
+
case 'g':
|
|
317
|
+
throw createCodexMatcherSyntaxError(state, 'backreferences are not supported by Codex matchers');
|
|
318
|
+
default:
|
|
319
|
+
if (/[A-Za-z<>]/.test(escaped)) {
|
|
320
|
+
throw createCodexMatcherSyntaxError(state, `unsupported escape \\${escaped}`);
|
|
321
|
+
}
|
|
322
|
+
state.index += 1;
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function consumeCodexMatcherPosixCharacterClass(state) {
|
|
327
|
+
state.index += 1; // [
|
|
328
|
+
if (state.source[state.index] !== ':') {
|
|
329
|
+
throw createCodexMatcherSyntaxError(state, 'unsupported nested character class');
|
|
330
|
+
}
|
|
331
|
+
state.index += 1; // :
|
|
332
|
+
if (state.source[state.index] === '^') {
|
|
333
|
+
state.index += 1;
|
|
334
|
+
}
|
|
335
|
+
let sawContent = false;
|
|
336
|
+
while (state.index < state.source.length) {
|
|
337
|
+
const char = state.source[state.index];
|
|
338
|
+
if (char === ':') {
|
|
339
|
+
if (state.source[state.index + 1] !== ']') {
|
|
340
|
+
throw createCodexMatcherSyntaxError(state, 'unterminated POSIX character class');
|
|
341
|
+
}
|
|
342
|
+
if (!sawContent) {
|
|
343
|
+
throw createCodexMatcherSyntaxError(state, 'empty POSIX character class');
|
|
344
|
+
}
|
|
345
|
+
state.index += 2;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (char === '[' || char === ']' || char === '\\') {
|
|
349
|
+
throw createCodexMatcherSyntaxError(state, 'unterminated POSIX character class');
|
|
350
|
+
}
|
|
351
|
+
sawContent = true;
|
|
352
|
+
state.index += 1;
|
|
353
|
+
}
|
|
354
|
+
throw createCodexMatcherSyntaxError(state, 'unterminated POSIX character class');
|
|
355
|
+
}
|
|
356
|
+
function consumeCodexMatcherCharacterClass(state) {
|
|
357
|
+
state.index += 1; // [
|
|
358
|
+
if (state.source[state.index] === '^') {
|
|
359
|
+
state.index += 1;
|
|
360
|
+
}
|
|
361
|
+
if (state.source[state.index] === ']') {
|
|
362
|
+
state.index += 1;
|
|
363
|
+
}
|
|
364
|
+
while (state.index < state.source.length) {
|
|
365
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
366
|
+
const char = state.source[state.index];
|
|
367
|
+
if (char === undefined) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (char === ']') {
|
|
371
|
+
state.index += 1;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (char === '\\') {
|
|
375
|
+
consumeCodexMatcherEscape(state, true);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (char === '[' && state.source[state.index + 1] === ':') {
|
|
379
|
+
consumeCodexMatcherPosixCharacterClass(state);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (char === '[') {
|
|
383
|
+
consumeCodexMatcherCharacterClass(state);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
state.index += 1;
|
|
387
|
+
}
|
|
388
|
+
throw createCodexMatcherSyntaxError(state, 'unterminated character class');
|
|
389
|
+
}
|
|
390
|
+
function consumeCodexMatcherFlags(state) {
|
|
391
|
+
const flags = cloneCodexMatcherFlags(currentCodexMatcherFlags(state));
|
|
392
|
+
let enabledCount = 0;
|
|
393
|
+
while (CODEX_REGEX_FLAG_SET.has(state.source[state.index] ?? '')) {
|
|
394
|
+
if (state.source[state.index] === 'x') {
|
|
395
|
+
flags.x = true;
|
|
396
|
+
}
|
|
397
|
+
enabledCount += 1;
|
|
398
|
+
state.index += 1;
|
|
399
|
+
}
|
|
400
|
+
let disabledCount = 0;
|
|
401
|
+
if (state.source[state.index] === '-') {
|
|
402
|
+
state.index += 1;
|
|
403
|
+
while (CODEX_REGEX_FLAG_SET.has(state.source[state.index] ?? '')) {
|
|
404
|
+
if (state.source[state.index] === 'x') {
|
|
405
|
+
flags.x = false;
|
|
406
|
+
}
|
|
407
|
+
disabledCount += 1;
|
|
408
|
+
state.index += 1;
|
|
409
|
+
}
|
|
410
|
+
if (disabledCount === 0) {
|
|
411
|
+
throw createCodexMatcherSyntaxError(state, 'expected flags after "-"');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (enabledCount === 0 && disabledCount === 0) {
|
|
415
|
+
throw createCodexMatcherSyntaxError(state, 'expected inline regex flags');
|
|
416
|
+
}
|
|
417
|
+
return flags;
|
|
418
|
+
}
|
|
419
|
+
function tryConsumeCodexMatcherCountedRepetition(state) {
|
|
420
|
+
const startIndex = state.index;
|
|
421
|
+
if (state.source[state.index] !== '{') {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
state.index += 1;
|
|
425
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
426
|
+
let lower = '';
|
|
427
|
+
while (/[0-9]/.test(state.source[state.index] ?? '')) {
|
|
428
|
+
lower += state.source[state.index];
|
|
429
|
+
state.index += 1;
|
|
430
|
+
}
|
|
431
|
+
if (lower.length === 0) {
|
|
432
|
+
state.index = startIndex;
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
436
|
+
if (state.source[state.index] === '}') {
|
|
437
|
+
state.index += 1;
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if (state.source[state.index] !== ',') {
|
|
441
|
+
state.index = startIndex;
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
state.index += 1;
|
|
445
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
446
|
+
let upper = '';
|
|
447
|
+
while (/[0-9]/.test(state.source[state.index] ?? '')) {
|
|
448
|
+
upper += state.source[state.index];
|
|
449
|
+
state.index += 1;
|
|
450
|
+
}
|
|
451
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
452
|
+
if (state.source[state.index] !== '}') {
|
|
453
|
+
state.index = startIndex;
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if (upper.length > 0 && Number(lower) > Number(upper)) {
|
|
457
|
+
throw createCodexMatcherSyntaxError(state, 'invalid counted repetition range');
|
|
458
|
+
}
|
|
459
|
+
state.index += 1;
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
function parseCodexMatcherSequence(state, closingChar) {
|
|
463
|
+
while (true) {
|
|
464
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
465
|
+
if (state.index >= state.source.length) {
|
|
466
|
+
if (closingChar) {
|
|
467
|
+
throw createCodexMatcherSyntaxError(state, `unterminated group, expected "${closingChar}"`);
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const char = state.source[state.index];
|
|
472
|
+
if (char === '|') {
|
|
473
|
+
state.index += 1;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (char === ')') {
|
|
477
|
+
if (!closingChar) {
|
|
478
|
+
throw createCodexMatcherSyntaxError(state, 'unmatched ")"');
|
|
479
|
+
}
|
|
480
|
+
state.index += 1;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const quantifiable = parseCodexMatcherAtom(state);
|
|
484
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
485
|
+
const next = state.source[state.index];
|
|
486
|
+
if (next === '*' || next === '+' || next === '?') {
|
|
487
|
+
if (!quantifiable) {
|
|
488
|
+
throw createCodexMatcherSyntaxError(state, 'dangling quantifier');
|
|
489
|
+
}
|
|
490
|
+
state.index += 1;
|
|
491
|
+
if (state.source[state.index] === '?') {
|
|
492
|
+
state.index += 1;
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (next === '{' && tryConsumeCodexMatcherCountedRepetition(state)) {
|
|
497
|
+
if (!quantifiable) {
|
|
498
|
+
throw createCodexMatcherSyntaxError(state, 'dangling quantifier');
|
|
499
|
+
}
|
|
500
|
+
if (state.source[state.index] === '?') {
|
|
501
|
+
state.index += 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function parseCodexMatcherGroup(state) {
|
|
507
|
+
state.index += 1; // (
|
|
508
|
+
if (state.source[state.index] !== '?') {
|
|
509
|
+
state.flagStack.push(cloneCodexMatcherFlags(currentCodexMatcherFlags(state)));
|
|
510
|
+
parseCodexMatcherSequence(state, ')');
|
|
511
|
+
state.flagStack.pop();
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
state.index += 1; // ?
|
|
515
|
+
const char = state.source[state.index];
|
|
516
|
+
if (char === '=' || char === '!') {
|
|
517
|
+
throw createCodexMatcherSyntaxError(state, 'look-around assertions are not supported by Codex matchers');
|
|
518
|
+
}
|
|
519
|
+
if (char === ':') {
|
|
520
|
+
state.index += 1;
|
|
521
|
+
state.flagStack.push(cloneCodexMatcherFlags(currentCodexMatcherFlags(state)));
|
|
522
|
+
parseCodexMatcherSequence(state, ')');
|
|
523
|
+
state.flagStack.pop();
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
if (char === '<') {
|
|
527
|
+
const next = state.source[state.index + 1];
|
|
528
|
+
if (next === '=' || next === '!') {
|
|
529
|
+
throw createCodexMatcherSyntaxError(state, 'look-around assertions are not supported by Codex matchers');
|
|
530
|
+
}
|
|
531
|
+
state.index += 1;
|
|
532
|
+
consumeCodexMatcherCaptureName(state);
|
|
533
|
+
if (state.source[state.index] !== '>') {
|
|
534
|
+
throw createCodexMatcherSyntaxError(state, 'unterminated capture group name');
|
|
535
|
+
}
|
|
536
|
+
state.index += 1;
|
|
537
|
+
state.flagStack.push(cloneCodexMatcherFlags(currentCodexMatcherFlags(state)));
|
|
538
|
+
parseCodexMatcherSequence(state, ')');
|
|
539
|
+
state.flagStack.pop();
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (char === 'P' && state.source[state.index + 1] === '<') {
|
|
543
|
+
state.index += 2;
|
|
544
|
+
consumeCodexMatcherCaptureName(state);
|
|
545
|
+
if (state.source[state.index] !== '>') {
|
|
546
|
+
throw createCodexMatcherSyntaxError(state, 'unterminated capture group name');
|
|
547
|
+
}
|
|
548
|
+
state.index += 1;
|
|
549
|
+
state.flagStack.push(cloneCodexMatcherFlags(currentCodexMatcherFlags(state)));
|
|
550
|
+
parseCodexMatcherSequence(state, ')');
|
|
551
|
+
state.flagStack.pop();
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
const flags = consumeCodexMatcherFlags(state);
|
|
555
|
+
if (state.source[state.index] === ')') {
|
|
556
|
+
state.flagStack[state.flagStack.length - 1] = flags;
|
|
557
|
+
state.index += 1;
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if (state.source[state.index] !== ':') {
|
|
561
|
+
throw createCodexMatcherSyntaxError(state, 'unsupported group construct');
|
|
562
|
+
}
|
|
563
|
+
state.index += 1;
|
|
564
|
+
state.flagStack.push(flags);
|
|
565
|
+
parseCodexMatcherSequence(state, ')');
|
|
566
|
+
state.flagStack.pop();
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
function parseCodexMatcherAtom(state) {
|
|
570
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
571
|
+
const char = state.source[state.index];
|
|
572
|
+
if (char === undefined) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
if (char === '*' || char === '+' || char === '?') {
|
|
576
|
+
throw createCodexMatcherSyntaxError(state, 'dangling quantifier');
|
|
577
|
+
}
|
|
578
|
+
if (char === '{' && tryConsumeCodexMatcherCountedRepetition(state)) {
|
|
579
|
+
throw createCodexMatcherSyntaxError(state, 'dangling quantifier');
|
|
580
|
+
}
|
|
581
|
+
if (char === '(') {
|
|
582
|
+
return parseCodexMatcherGroup(state);
|
|
583
|
+
}
|
|
584
|
+
if (char === '[') {
|
|
585
|
+
consumeCodexMatcherCharacterClass(state);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
if (char === '\\') {
|
|
589
|
+
return consumeCodexMatcherEscape(state);
|
|
590
|
+
}
|
|
591
|
+
if (char === '^' || char === '$') {
|
|
592
|
+
state.index += 1;
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
state.index += 1;
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
function assertCodexRegexSyntax(matcher) {
|
|
599
|
+
const state = {
|
|
600
|
+
source: matcher,
|
|
601
|
+
index: 0,
|
|
602
|
+
flagStack: [{ x: false }],
|
|
603
|
+
};
|
|
604
|
+
parseCodexMatcherSequence(state);
|
|
605
|
+
advanceCodexMatcherVerboseTrivia(state);
|
|
606
|
+
if (state.index !== matcher.length) {
|
|
607
|
+
throw createCodexMatcherSyntaxError(state, 'unexpected trailing token');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Codex matcher semantics source:
|
|
611
|
+
// - codex-rs/hooks/src/events/common.rs: validate_matcher_pattern / matches_matcher
|
|
612
|
+
// - codex-rs/hooks/src/engine/discovery.rs: invalid matcher groups are skipped
|
|
613
|
+
// Verified against codex-rs on 2026-04-28.
|
|
614
|
+
function getCodexMatcherPatternError(matcher) {
|
|
615
|
+
if (matcher === '' ||
|
|
616
|
+
matcher === '*' ||
|
|
617
|
+
CODEX_EXACT_MATCHER_PATTERN.test(matcher)) {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
// Codex runtime uses Rust regex, so validation here follows Codex's
|
|
622
|
+
// exact-token fast path plus a dedicated syntax parser instead of JS RegExp.
|
|
623
|
+
assertCodexRegexSyntax(matcher);
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
return error instanceof Error ? error.message : String(error);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function assertCodexMatcherPattern(matcher, fieldName) {
|
|
631
|
+
const errorMessage = getCodexMatcherPatternError(matcher);
|
|
632
|
+
if (errorMessage === undefined) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
throw new Error(`Invalid ${fieldName}: matcher ${JSON.stringify(matcher)} is not a valid Codex matcher pattern (${errorMessage}).`);
|
|
636
|
+
}
|
|
637
|
+
function normalizeCodexSharedMatcherValue(matcher, matcherAllowed) {
|
|
638
|
+
if (!matcherAllowed) {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
if (matcher === undefined || matcher === null || matcher === '' || matcher === '*') {
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
return matcher;
|
|
645
|
+
}
|
|
646
|
+
function normalizeStrictMatcherValue(matcher, matcherAllowed, platform, fieldName) {
|
|
647
|
+
if (!matcherAllowed) {
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
if (matcher === undefined) {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
if (platform === 'codex') {
|
|
654
|
+
assertCodexMatcherPattern(matcher, fieldName);
|
|
655
|
+
if (matcher === '' || matcher === '*') {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
return matcher;
|
|
659
|
+
}
|
|
660
|
+
if (matcher === '*') {
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
return matcher;
|
|
664
|
+
}
|
|
665
|
+
function createTrackedHookGroupIdentityCounts(identities) {
|
|
666
|
+
const counts = new Map();
|
|
667
|
+
for (const identity of identities) {
|
|
668
|
+
const key = serializeTrackedHookGroupIdentity(identity);
|
|
669
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
670
|
+
}
|
|
671
|
+
return counts;
|
|
672
|
+
}
|
|
673
|
+
export function assertPublicHookEvent(value, fieldName) {
|
|
674
|
+
const event = assertNonEmptyString(value, fieldName);
|
|
675
|
+
if (event === 'session_stop') {
|
|
676
|
+
throw new Error(`Invalid ${fieldName}: public event "session_stop" has been removed. Use extensions.nativeHooks.claude[].event = "Stop" or "SessionEnd", or extensions.nativeHooks.codex[].event = "Stop".`);
|
|
677
|
+
}
|
|
678
|
+
if (!PUBLIC_HOOK_EVENT_SET.has(event)) {
|
|
679
|
+
throw new Error(`Invalid ${fieldName}: unsupported hook event "${event}".`);
|
|
680
|
+
}
|
|
681
|
+
return event;
|
|
682
|
+
}
|
|
683
|
+
export function renderPublicHookEvent(event, platform) {
|
|
684
|
+
return PUBLIC_HOOK_EVENT_RENDER_MAP[event][platform];
|
|
685
|
+
}
|
|
686
|
+
export function assertNativeHookEvent(event, platform, fieldName) {
|
|
687
|
+
assertNonEmptyString(event, fieldName);
|
|
688
|
+
if (!NATIVE_HOOK_EVENT_SETS[platform].has(event)) {
|
|
689
|
+
throw new Error(`Invalid ${fieldName}: unsupported ${platform} hook event "${event}".`);
|
|
690
|
+
}
|
|
691
|
+
return event;
|
|
692
|
+
}
|
|
693
|
+
export function assertHookMatcherAllowed(options) {
|
|
694
|
+
if (options.kind === 'public') {
|
|
695
|
+
if (options.event !== 'session_start' || options.matcher === undefined) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
normalizePublicSessionStartMatcher(options.matcher, options.fieldName);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const contract = getNativeHookEventContract(options.event, options.platform, options.fieldName);
|
|
702
|
+
if (!contract.matcherAllowed && options.matcher !== undefined) {
|
|
703
|
+
throw new Error(`Invalid ${options.fieldName}: ${options.platform} event "${options.event}" does not allow matcher.`);
|
|
704
|
+
}
|
|
705
|
+
if (contract.matcherAllowed &&
|
|
706
|
+
options.platform === 'codex' &&
|
|
707
|
+
options.matcher !== undefined) {
|
|
708
|
+
assertCodexMatcherPattern(options.matcher, options.fieldName);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
export function resolvePlatformHookDefinitions(manifest, platform) {
|
|
712
|
+
const publicHooks = resolvePlatformEntries(manifest.extensions?.hooks, platform).map((hook) => ({
|
|
713
|
+
kind: 'public',
|
|
714
|
+
event: hook.event,
|
|
715
|
+
matcher: hook.matcher,
|
|
716
|
+
command: hook.command,
|
|
717
|
+
}));
|
|
718
|
+
const nativeHooks = (manifest.extensions?.nativeHooks?.[platform] ?? []).map((hook) => ({
|
|
719
|
+
kind: 'native',
|
|
720
|
+
event: hook.event,
|
|
721
|
+
matcher: hook.matcher,
|
|
722
|
+
command: hook.command,
|
|
723
|
+
}));
|
|
724
|
+
return [...publicHooks, ...nativeHooks];
|
|
725
|
+
}
|
|
726
|
+
export function renderHookDefinitionGroup(definition, platform) {
|
|
727
|
+
const event = definition.kind === 'public'
|
|
728
|
+
? renderPublicHookEvent(definition.event, platform)
|
|
729
|
+
: definition.event;
|
|
730
|
+
let matcher;
|
|
731
|
+
if (definition.kind === 'public' && definition.event === 'session_start') {
|
|
732
|
+
matcher = definition.matcher === undefined
|
|
733
|
+
? PUBLIC_SESSION_START_DEFAULT_MATCHER
|
|
734
|
+
: normalizePublicSessionStartMatcher(definition.matcher, `${platform}.SessionStart.matcher`);
|
|
735
|
+
}
|
|
736
|
+
else if (definition.matcher !== undefined) {
|
|
737
|
+
matcher = definition.matcher;
|
|
738
|
+
}
|
|
739
|
+
else if (getNativeHookEventContract(event, platform, `${platform}.${event}`).matcherAllowed) {
|
|
740
|
+
matcher = '*';
|
|
741
|
+
}
|
|
742
|
+
const rendered = {
|
|
743
|
+
event,
|
|
744
|
+
hooks: [{ type: 'command', command: definition.command }],
|
|
745
|
+
};
|
|
746
|
+
if (matcher !== undefined) {
|
|
747
|
+
rendered.matcher = matcher;
|
|
748
|
+
}
|
|
749
|
+
return rendered;
|
|
750
|
+
}
|
|
751
|
+
export function normalizeRenderedHookGroupIdentity(options) {
|
|
752
|
+
const command = assertNonEmptyString(options.command, `${options.platform}.${options.event}.command`);
|
|
753
|
+
const contract = getNativeHookEventContract(options.event, options.platform, `${options.platform}.${options.event}`);
|
|
754
|
+
let matcher;
|
|
755
|
+
if (options.mode === 'codex-shared-read') {
|
|
756
|
+
if (options.matcher !== undefined &&
|
|
757
|
+
options.matcher !== null &&
|
|
758
|
+
typeof options.matcher !== 'string') {
|
|
759
|
+
throw new Error(`Invalid ${options.platform}.${options.event}.matcher: expected undefined, null, or string.`);
|
|
760
|
+
}
|
|
761
|
+
matcher = normalizeCodexSharedMatcherValue(options.matcher, contract.matcherAllowed);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
if (options.matcher !== undefined && options.matcher !== null && typeof options.matcher !== 'string') {
|
|
765
|
+
throw new Error(`Invalid ${options.platform}.${options.event}.matcher: expected a string.`);
|
|
766
|
+
}
|
|
767
|
+
if (!contract.matcherAllowed && options.matcher !== undefined) {
|
|
768
|
+
throw new Error(`Invalid ${options.platform}.${options.event}.matcher: event "${options.event}" does not allow matcher.`);
|
|
769
|
+
}
|
|
770
|
+
matcher = normalizeStrictMatcherValue(options.matcher ?? undefined, contract.matcherAllowed, options.platform, `${options.platform}.${options.event}.matcher`);
|
|
771
|
+
}
|
|
772
|
+
return matcher === undefined
|
|
773
|
+
? { event: options.event, command }
|
|
774
|
+
: { event: options.event, matcher, command };
|
|
775
|
+
}
|
|
776
|
+
export function serializeTrackedHookGroupIdentity(identity) {
|
|
777
|
+
return JSON.stringify([
|
|
778
|
+
identity.event,
|
|
779
|
+
identity.matcher ?? null,
|
|
780
|
+
identity.command,
|
|
781
|
+
]);
|
|
782
|
+
}
|
|
783
|
+
export function renderTrackedHookGroupIdentity(identity) {
|
|
784
|
+
return {
|
|
785
|
+
event: identity.event,
|
|
786
|
+
...(identity.matcher === undefined ? {} : { matcher: identity.matcher }),
|
|
787
|
+
hooks: [{ type: 'command', command: identity.command }],
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
export function trackedHookGroupsContainAll(actualGroups, expectedGroups) {
|
|
791
|
+
const actualCounts = createTrackedHookGroupIdentityCounts(actualGroups);
|
|
792
|
+
for (const identity of expectedGroups) {
|
|
793
|
+
const key = serializeTrackedHookGroupIdentity(identity);
|
|
794
|
+
const remaining = actualCounts.get(key) ?? 0;
|
|
795
|
+
if (remaining === 0) {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
actualCounts.set(key, remaining - 1);
|
|
799
|
+
}
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
export function subtractTrackedHookGroupIdentities(groups, removedGroups) {
|
|
803
|
+
const removedCounts = createTrackedHookGroupIdentityCounts(removedGroups);
|
|
804
|
+
return groups.filter((group) => {
|
|
805
|
+
const key = serializeTrackedHookGroupIdentity(group);
|
|
806
|
+
const remaining = removedCounts.get(key) ?? 0;
|
|
807
|
+
if (remaining === 0) {
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
removedCounts.set(key, remaining - 1);
|
|
811
|
+
return false;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
function isMinimalManagedCodexGroup(groupRecord, handlers) {
|
|
815
|
+
if (handlers.length !== 1) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
const handler = handlers[0];
|
|
819
|
+
const allowedGroupKeys = new Set(['matcher', 'hooks']);
|
|
820
|
+
if (Object.keys(groupRecord).some((key) => !allowedGroupKeys.has(key))) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
if (handler.type !== 'command' || typeof handler.command !== 'string' || handler.command.trim() === '') {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
const allowedHandlerKeys = new Set(['type', 'command']);
|
|
827
|
+
return !Object.keys(handler).some((key) => !allowedHandlerKeys.has(key));
|
|
828
|
+
}
|
|
829
|
+
function assertCodexSharedCommandHandler(handler, fieldName) {
|
|
830
|
+
if (typeof handler.command !== 'string' || handler.command.trim() === '') {
|
|
831
|
+
throw new Error(`Invalid ${fieldName}.command: expected a non-empty string.`);
|
|
832
|
+
}
|
|
833
|
+
if (handler.timeout !== undefined &&
|
|
834
|
+
(!Number.isInteger(handler.timeout) || handler.timeout < 0)) {
|
|
835
|
+
throw new Error(`Invalid ${fieldName}.timeout: expected a non-negative integer.`);
|
|
836
|
+
}
|
|
837
|
+
if (handler.async !== undefined && typeof handler.async !== 'boolean') {
|
|
838
|
+
throw new Error(`Invalid ${fieldName}.async: expected a boolean.`);
|
|
839
|
+
}
|
|
840
|
+
if (handler.statusMessage !== undefined &&
|
|
841
|
+
typeof handler.statusMessage !== 'string') {
|
|
842
|
+
throw new Error(`Invalid ${fieldName}.statusMessage: expected a string.`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function decrementIdentityCount(counts, identity) {
|
|
846
|
+
if (!identity) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
const key = serializeTrackedHookGroupIdentity(identity);
|
|
850
|
+
const remaining = counts.get(key) ?? 0;
|
|
851
|
+
if (remaining === 0) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
counts.set(key, remaining - 1);
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
export function normalizeCodexSharedHookGroupForRead(event, value, fieldName) {
|
|
858
|
+
assertNativeHookEvent(event, 'codex', `${fieldName}.event`);
|
|
859
|
+
const contract = getNativeHookEventContract(event, 'codex', `${fieldName}.event`);
|
|
860
|
+
const groupRecord = assertObject(value, fieldName);
|
|
861
|
+
if (!Array.isArray(groupRecord.hooks)) {
|
|
862
|
+
throw new Error(`Invalid ${fieldName}.hooks: expected an array.`);
|
|
863
|
+
}
|
|
864
|
+
const matcher = Object.prototype.hasOwnProperty.call(groupRecord, 'matcher')
|
|
865
|
+
? groupRecord.matcher
|
|
866
|
+
: undefined;
|
|
867
|
+
if (matcher !== undefined &&
|
|
868
|
+
matcher !== null &&
|
|
869
|
+
typeof matcher !== 'string') {
|
|
870
|
+
throw new Error(`Invalid ${fieldName}.matcher: expected undefined, null, or string.`);
|
|
871
|
+
}
|
|
872
|
+
const handlers = groupRecord.hooks.map((handler, index) => assertObject(handler, `${fieldName}.hooks[${index}]`));
|
|
873
|
+
for (const [index, handler] of handlers.entries()) {
|
|
874
|
+
const type = handler.type;
|
|
875
|
+
if (type !== 'command' && type !== 'prompt' && type !== 'agent') {
|
|
876
|
+
throw new Error(`Invalid ${fieldName}.hooks[${index}].type: expected "command", "prompt", or "agent".`);
|
|
877
|
+
}
|
|
878
|
+
if (type === 'command') {
|
|
879
|
+
assertCodexSharedCommandHandler(handler, `${fieldName}.hooks[${index}]`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const rawGroup = {
|
|
883
|
+
event,
|
|
884
|
+
rawGroup: groupRecord,
|
|
885
|
+
hooks: handlers,
|
|
886
|
+
...(Object.prototype.hasOwnProperty.call(groupRecord, 'matcher') ? { matcher } : {}),
|
|
887
|
+
};
|
|
888
|
+
if (!isMinimalManagedCodexGroup(groupRecord, handlers)) {
|
|
889
|
+
return { rawGroup };
|
|
890
|
+
}
|
|
891
|
+
if (contract.matcherAllowed &&
|
|
892
|
+
typeof matcher === 'string' &&
|
|
893
|
+
getCodexMatcherPatternError(matcher) !== undefined) {
|
|
894
|
+
return { rawGroup };
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
rawGroup,
|
|
898
|
+
managedIdentity: normalizeRenderedHookGroupIdentity({
|
|
899
|
+
platform: 'codex',
|
|
900
|
+
event,
|
|
901
|
+
matcher,
|
|
902
|
+
command: handlers[0].command,
|
|
903
|
+
mode: 'codex-shared-read',
|
|
904
|
+
}),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
export function parseCodexSharedHooksFile(content, fieldName = 'codex.hooks') {
|
|
908
|
+
const root = assertObject(content, fieldName);
|
|
909
|
+
const hooks = assertObject(root.hooks, `${fieldName}.hooks`);
|
|
910
|
+
const groups = [];
|
|
911
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
912
|
+
if (!Array.isArray(entries)) {
|
|
913
|
+
throw new Error(`Invalid ${fieldName}.hooks.${event}: expected an array.`);
|
|
914
|
+
}
|
|
915
|
+
for (const [index, entry] of entries.entries()) {
|
|
916
|
+
groups.push(normalizeCodexSharedHookGroupForRead(event, entry, `${fieldName}.hooks.${event}[${index}]`));
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
groups,
|
|
921
|
+
rawGroups: groups.map((group) => group.rawGroup),
|
|
922
|
+
managedIdentities: groups
|
|
923
|
+
.flatMap((group) => (group.managedIdentity ? [group.managedIdentity] : [])),
|
|
924
|
+
opaqueRawGroups: groups
|
|
925
|
+
.flatMap((group) => (group.managedIdentity ? [] : [group.rawGroup])),
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
export function planCodexSharedHooksUpdate(parsed, options) {
|
|
929
|
+
const rerenderedCounts = createTrackedHookGroupIdentityCounts(options.rerenderedIdentities);
|
|
930
|
+
const historicalCounts = createTrackedHookGroupIdentityCounts(options.historicalIdentities ?? []);
|
|
931
|
+
const rerenderedAwRawGroups = [];
|
|
932
|
+
const preservedHistoricalAwRawGroups = [];
|
|
933
|
+
const preservedForeignRawGroups = [];
|
|
934
|
+
for (const group of parsed.groups) {
|
|
935
|
+
if (decrementIdentityCount(rerenderedCounts, group.managedIdentity)) {
|
|
936
|
+
rerenderedAwRawGroups.push(group.rawGroup);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (decrementIdentityCount(historicalCounts, group.managedIdentity)) {
|
|
940
|
+
preservedHistoricalAwRawGroups.push(group.rawGroup);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
preservedForeignRawGroups.push(group.rawGroup);
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
rerenderedAwRawGroups,
|
|
947
|
+
preservedHistoricalAwRawGroups,
|
|
948
|
+
preservedForeignRawGroups,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
export function normalizeNativeHookDefinition(entry) {
|
|
952
|
+
return {
|
|
953
|
+
kind: 'native',
|
|
954
|
+
event: entry.event,
|
|
955
|
+
matcher: entry.matcher,
|
|
956
|
+
command: entry.command,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
export function normalizePublicHookDefinition(entry) {
|
|
960
|
+
return {
|
|
961
|
+
kind: 'public',
|
|
962
|
+
event: entry.event,
|
|
963
|
+
matcher: entry.matcher,
|
|
964
|
+
command: entry.command,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
//# sourceMappingURL=hooks-contract.js.map
|