@ghl-ai/aw 0.1.39-beta.8 → 0.1.39
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/cli.mjs +8 -1
- package/codex.mjs +839 -0
- package/commands/doctor.mjs +1086 -0
- package/commands/init.mjs +71 -81
- package/commands/link-project.mjs +12 -1
- package/commands/nuke.mjs +14 -4
- package/commands/pull.mjs +111 -11
- package/commands/push-rules.mjs +4 -15
- package/commands/push.mjs +4 -4
- package/commands/search.mjs +1 -1
- package/commands/startup.mjs +87 -0
- package/constants.mjs +3 -1
- package/ecc.mjs +130 -42
- package/git.mjs +4 -23
- package/hook-manifest.mjs +195 -0
- package/hooks/codex-home.mjs +184 -0
- package/hooks/shared-phase-scripts.mjs +69 -0
- package/integrate.mjs +219 -47
- package/link.mjs +36 -1
- package/mcp.mjs +2 -10
- package/package.json +8 -2
- package/paths.mjs +1 -1
- package/registry.mjs +1 -1
- package/render-rules.mjs +267 -27
- package/startup.mjs +562 -0
package/codex.mjs
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
// codex.mjs — Project-local Codex defaults for AW/ECC startup.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { getSupportedHarnessPhaseEntries } from './hook-manifest.mjs';
|
|
7
|
+
|
|
8
|
+
const CODEX_HOOK_MATCHER = 'startup|resume';
|
|
9
|
+
const CODEX_HOOK_COMMAND = 'bash "$(git rev-parse --show-toplevel)/hooks/aw-session-start"';
|
|
10
|
+
const CODEX_PROMPT_COMMAND = 'bash "$(git rev-parse --show-toplevel)/scripts/hooks/session-start-rules-context.sh"';
|
|
11
|
+
const CODEX_HOOK_STATUS = 'Loading AW router';
|
|
12
|
+
const CURSOR_HOOK_COMMAND = 'bash "$(git rev-parse --show-toplevel)/hooks/aw-session-start"';
|
|
13
|
+
const CLAUDE_HOOK_MATCHER = 'startup|resume|clear|compact';
|
|
14
|
+
const CLAUDE_HOOK_COMMAND = '"$CLAUDE_PROJECT_DIR"/hooks/aw-session-start';
|
|
15
|
+
const CLAUDE_PROMPT_COMMAND = 'bash "${CLAUDE_PROJECT_DIR}/scripts/hooks/session-start-rules-context.sh"';
|
|
16
|
+
|
|
17
|
+
const CONFIG_FILE_MARKER = '# aw-managed-file: codex-ecc-baseline';
|
|
18
|
+
const HOOK_LINE_MARKER = '# aw-managed: codex_hooks';
|
|
19
|
+
const HOOK_LINE_REPLACED_MARKER = '# aw-managed: codex_hooks restored-from-false';
|
|
20
|
+
const FEATURES_BLOCK_NAME = 'features';
|
|
21
|
+
const AW_SESSION_START_MARKER = '# aw-managed: codex-aw-session-start';
|
|
22
|
+
const SUPPORTED_CODEX_WORKSPACE_PHASES = new Set(
|
|
23
|
+
getSupportedHarnessPhaseEntries('codex', 'workspace').map(entry => entry.phase),
|
|
24
|
+
);
|
|
25
|
+
const SUPPORTED_CURSOR_WORKSPACE_PHASES = new Set(
|
|
26
|
+
getSupportedHarnessPhaseEntries('cursor', 'workspace').map(entry => entry.phase),
|
|
27
|
+
);
|
|
28
|
+
const SUPPORTED_CLAUDE_WORKSPACE_PHASES = new Set(
|
|
29
|
+
getSupportedHarnessPhaseEntries('claude', 'workspace').map(entry => entry.phase),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Keep the AWCLI baseline lean and collision-free. Project-specific bridges such
|
|
33
|
+
// as `ghl-ai` belong in the repo's own `.codex/config.toml`, not in the managed
|
|
34
|
+
// defaults that AWCLI installs into every linked project.
|
|
35
|
+
const CODEX_MANAGED_SERVERS = [
|
|
36
|
+
{
|
|
37
|
+
name: 'github',
|
|
38
|
+
lines: [
|
|
39
|
+
'[mcp_servers.github]',
|
|
40
|
+
'command = "npx"',
|
|
41
|
+
'args = ["-y", "@modelcontextprotocol/server-github"]',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'context7-mcp',
|
|
46
|
+
lines: [
|
|
47
|
+
'[mcp_servers.context7-mcp]',
|
|
48
|
+
'command = "npx"',
|
|
49
|
+
'args = ["-y", "@upstash/context7-mcp@latest"]',
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'exa',
|
|
54
|
+
lines: [
|
|
55
|
+
'[mcp_servers.exa]',
|
|
56
|
+
'url = "https://mcp.exa.ai/mcp"',
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'memory',
|
|
61
|
+
lines: [
|
|
62
|
+
'[mcp_servers.memory]',
|
|
63
|
+
'command = "npx"',
|
|
64
|
+
'args = ["-y", "@modelcontextprotocol/server-memory"]',
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'playwright',
|
|
69
|
+
lines: [
|
|
70
|
+
'[mcp_servers.playwright]',
|
|
71
|
+
'command = "npx"',
|
|
72
|
+
'args = ["-y", "@playwright/mcp@latest", "--extension"]',
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'sequential-thinking',
|
|
77
|
+
lines: [
|
|
78
|
+
'[mcp_servers.sequential-thinking]',
|
|
79
|
+
'command = "npx"',
|
|
80
|
+
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const AW_SESSION_START_SCRIPT = `#!/usr/bin/env bash
|
|
86
|
+
${AW_SESSION_START_MARKER}
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
|
|
89
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
90
|
+
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
91
|
+
TARGET_SCRIPT="\$ROOT_DIR/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"
|
|
92
|
+
|
|
93
|
+
if [[ ! -f "\$TARGET_SCRIPT" ]]; then
|
|
94
|
+
echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "WARNING: AW session-start hook not found at .aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh. Run aw pull platform or aw init."}}'
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
exec bash "\$TARGET_SCRIPT"
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
export function ensureCodexWorkspaceDefaults(cwd) {
|
|
102
|
+
const updatedFiles = [];
|
|
103
|
+
|
|
104
|
+
const codexConfigPath = join(cwd, '.codex', 'config.toml');
|
|
105
|
+
if (ensureCodexConfig(codexConfigPath)) {
|
|
106
|
+
updatedFiles.push(codexConfigPath);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const codexHooksPath = join(cwd, '.codex', 'hooks.json');
|
|
110
|
+
if (ensureCodexHooksJson(codexHooksPath)) {
|
|
111
|
+
updatedFiles.push(codexHooksPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const hookWrapperPath = join(cwd, 'hooks', 'aw-session-start');
|
|
115
|
+
if (ensureManagedScript(hookWrapperPath, AW_SESSION_START_SCRIPT, AW_SESSION_START_MARKER)) {
|
|
116
|
+
updatedFiles.push(hookWrapperPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return updatedFiles;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function ensureWorkspaceHookDefaults(cwd) {
|
|
123
|
+
const updatedFiles = [...ensureCodexWorkspaceDefaults(cwd)];
|
|
124
|
+
|
|
125
|
+
const cursorHooksPath = join(cwd, '.cursor', 'hooks.json');
|
|
126
|
+
if (ensureCursorHooksJson(cursorHooksPath)) {
|
|
127
|
+
updatedFiles.push(cursorHooksPath);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const claudeSettingsPath = join(cwd, '.claude', 'settings.json');
|
|
131
|
+
if (ensureClaudeSettingsJson(claudeSettingsPath)) {
|
|
132
|
+
updatedFiles.push(claudeSettingsPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return updatedFiles;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function removeCodexWorkspaceDefaults(cwd) {
|
|
139
|
+
const removedFiles = [];
|
|
140
|
+
|
|
141
|
+
const hookWrapperPath = join(cwd, 'hooks', 'aw-session-start');
|
|
142
|
+
if (removeManagedScript(hookWrapperPath, AW_SESSION_START_MARKER)) {
|
|
143
|
+
removedFiles.push(hookWrapperPath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const codexHooksPath = join(cwd, '.codex', 'hooks.json');
|
|
147
|
+
if (removeManagedHooksJson(codexHooksPath)) {
|
|
148
|
+
removedFiles.push(codexHooksPath);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const codexConfigPath = join(cwd, '.codex', 'config.toml');
|
|
152
|
+
if (removeManagedCodexConfig(codexConfigPath)) {
|
|
153
|
+
removedFiles.push(codexConfigPath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pruneEmptyDir(join(cwd, '.codex'));
|
|
157
|
+
pruneEmptyDir(join(cwd, 'hooks'));
|
|
158
|
+
|
|
159
|
+
return removedFiles;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function removeWorkspaceHookDefaults(cwd) {
|
|
163
|
+
const removedFiles = [];
|
|
164
|
+
|
|
165
|
+
const cursorHooksPath = join(cwd, '.cursor', 'hooks.json');
|
|
166
|
+
if (removeManagedCursorHooksJson(cursorHooksPath)) {
|
|
167
|
+
removedFiles.push(cursorHooksPath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const claudeSettingsPath = join(cwd, '.claude', 'settings.json');
|
|
171
|
+
if (removeManagedClaudeSettingsJson(claudeSettingsPath)) {
|
|
172
|
+
removedFiles.push(claudeSettingsPath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
removedFiles.push(...removeCodexWorkspaceDefaults(cwd));
|
|
176
|
+
|
|
177
|
+
pruneEmptyDir(join(cwd, '.cursor'));
|
|
178
|
+
pruneEmptyDir(join(cwd, '.claude'));
|
|
179
|
+
|
|
180
|
+
return removedFiles;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ensureCodexConfig(filePath) {
|
|
184
|
+
if (!existsSync(filePath)) {
|
|
185
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
186
|
+
writeFileSync(filePath, buildManagedConfigFile());
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
191
|
+
if (isLegacyHookOnlyConfig(existing)) {
|
|
192
|
+
writeFileSync(filePath, buildManagedConfigFile());
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let next = existing;
|
|
197
|
+
next = ensureCodexHookEnabled(next);
|
|
198
|
+
next = ensureManagedServerBlocks(next);
|
|
199
|
+
|
|
200
|
+
if (next === existing) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
writeFileSync(filePath, next);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ensureCodexHooksJson(filePath) {
|
|
209
|
+
let config = {};
|
|
210
|
+
|
|
211
|
+
if (existsSync(filePath)) {
|
|
212
|
+
try {
|
|
213
|
+
config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
214
|
+
} catch {
|
|
215
|
+
config = {};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
220
|
+
config.hooks = {};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const sessionStart = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
|
|
224
|
+
const existingEntry = sessionStart.find(entry =>
|
|
225
|
+
entry?.matcher === CODEX_HOOK_MATCHER &&
|
|
226
|
+
Array.isArray(entry.hooks) &&
|
|
227
|
+
entry.hooks.some(hook => hook?.type === 'command' && hook?.command === CODEX_HOOK_COMMAND)
|
|
228
|
+
);
|
|
229
|
+
const existingAwEntry = sessionStart.find(entry =>
|
|
230
|
+
Array.isArray(entry?.hooks) &&
|
|
231
|
+
entry.hooks.some(hook => hook?.type === 'command' && isAwSessionStartCommand(hook?.command))
|
|
232
|
+
);
|
|
233
|
+
const promptSubmit = Array.isArray(config.hooks.UserPromptSubmit) ? config.hooks.UserPromptSubmit : [];
|
|
234
|
+
const existingPromptEntry = promptSubmit.find(entry =>
|
|
235
|
+
Array.isArray(entry?.hooks) &&
|
|
236
|
+
entry.hooks.some(hook => hook?.type === 'command' && isAwPromptReminderCommand(hook?.command))
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
let changed = false;
|
|
240
|
+
|
|
241
|
+
if (SUPPORTED_CODEX_WORKSPACE_PHASES.has('SessionStart') && existingEntry) {
|
|
242
|
+
const desiredHook = existingEntry.hooks.find(hook => hook?.type === 'command' && hook?.command === CODEX_HOOK_COMMAND);
|
|
243
|
+
if (desiredHook && desiredHook.statusMessage !== CODEX_HOOK_STATUS) {
|
|
244
|
+
desiredHook.statusMessage = CODEX_HOOK_STATUS;
|
|
245
|
+
changed = true;
|
|
246
|
+
}
|
|
247
|
+
} else if (SUPPORTED_CODEX_WORKSPACE_PHASES.has('SessionStart') && !existingAwEntry) {
|
|
248
|
+
sessionStart.push({
|
|
249
|
+
matcher: CODEX_HOOK_MATCHER,
|
|
250
|
+
hooks: [
|
|
251
|
+
{
|
|
252
|
+
type: 'command',
|
|
253
|
+
command: CODEX_HOOK_COMMAND,
|
|
254
|
+
statusMessage: CODEX_HOOK_STATUS,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
config.hooks.SessionStart = sessionStart;
|
|
259
|
+
changed = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (SUPPORTED_CODEX_WORKSPACE_PHASES.has('UserPromptSubmit') && !existingPromptEntry) {
|
|
263
|
+
promptSubmit.push({
|
|
264
|
+
hooks: [
|
|
265
|
+
{
|
|
266
|
+
type: 'command',
|
|
267
|
+
command: CODEX_PROMPT_COMMAND,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
config.hooks.UserPromptSubmit = promptSubmit;
|
|
272
|
+
changed = true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!changed && existsSync(filePath)) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
280
|
+
writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function ensureCursorHooksJson(filePath) {
|
|
285
|
+
let config = {};
|
|
286
|
+
|
|
287
|
+
if (existsSync(filePath)) {
|
|
288
|
+
try {
|
|
289
|
+
config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
290
|
+
} catch {
|
|
291
|
+
config = {};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
296
|
+
config = {};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let changed = false;
|
|
300
|
+
|
|
301
|
+
if (config.version === undefined) {
|
|
302
|
+
config.version = 1;
|
|
303
|
+
changed = true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
307
|
+
config.hooks = {};
|
|
308
|
+
changed = true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const sessionStart = Array.isArray(config.hooks.sessionStart) ? config.hooks.sessionStart : [];
|
|
312
|
+
const existingAwEntry = sessionStart.find(entry => isAwSessionStartCommand(entry?.command));
|
|
313
|
+
|
|
314
|
+
if (SUPPORTED_CURSOR_WORKSPACE_PHASES.has('SessionStart') && !existingAwEntry) {
|
|
315
|
+
sessionStart.push({ command: CURSOR_HOOK_COMMAND });
|
|
316
|
+
config.hooks.sessionStart = sessionStart;
|
|
317
|
+
changed = true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!changed && existsSync(filePath)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
325
|
+
writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function removeManagedCursorHooksJson(filePath) {
|
|
330
|
+
if (!existsSync(filePath)) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let config;
|
|
335
|
+
try {
|
|
336
|
+
config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
337
|
+
} catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!config || typeof config !== 'object' || Array.isArray(config) || !config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let changed = false;
|
|
346
|
+
|
|
347
|
+
if (Array.isArray(config.hooks.sessionStart)) {
|
|
348
|
+
const nextSessionStart = config.hooks.sessionStart.filter(entry => entry?.command !== CURSOR_HOOK_COMMAND);
|
|
349
|
+
if (nextSessionStart.length !== config.hooks.sessionStart.length) {
|
|
350
|
+
changed = true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (nextSessionStart.length > 0) {
|
|
354
|
+
config.hooks.sessionStart = nextSessionStart;
|
|
355
|
+
} else if (config.hooks.sessionStart.length > 0) {
|
|
356
|
+
delete config.hooks.sessionStart;
|
|
357
|
+
changed = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!changed) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (Object.keys(config.hooks).length === 0) {
|
|
366
|
+
delete config.hooks;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (isManagedJsonShell(config, ['version']) || isManagedJsonShell(config, [])) {
|
|
370
|
+
rmSync(filePath, { force: true });
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function ensureClaudeSettingsJson(filePath) {
|
|
379
|
+
let config = {};
|
|
380
|
+
|
|
381
|
+
if (existsSync(filePath)) {
|
|
382
|
+
try {
|
|
383
|
+
config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
384
|
+
} catch {
|
|
385
|
+
config = {};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
390
|
+
config = {};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
394
|
+
config.hooks = {};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const sessionStart = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
|
|
398
|
+
const existingAwEntry = sessionStart.find(entry =>
|
|
399
|
+
Array.isArray(entry?.hooks) &&
|
|
400
|
+
entry.hooks.some(hook => hook?.type === 'command' && isAwSessionStartCommand(hook?.command))
|
|
401
|
+
);
|
|
402
|
+
const promptSubmit = Array.isArray(config.hooks.UserPromptSubmit) ? config.hooks.UserPromptSubmit : [];
|
|
403
|
+
const existingPromptEntry = promptSubmit.find(entry =>
|
|
404
|
+
Array.isArray(entry?.hooks) &&
|
|
405
|
+
entry.hooks.some(hook => hook?.type === 'command' && isAwPromptReminderCommand(hook?.command))
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
let changed = false;
|
|
409
|
+
|
|
410
|
+
if (SUPPORTED_CLAUDE_WORKSPACE_PHASES.has('SessionStart') && !existingAwEntry) {
|
|
411
|
+
sessionStart.push({
|
|
412
|
+
matcher: CLAUDE_HOOK_MATCHER,
|
|
413
|
+
hooks: [
|
|
414
|
+
{
|
|
415
|
+
type: 'command',
|
|
416
|
+
command: CLAUDE_HOOK_COMMAND,
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
config.hooks.SessionStart = sessionStart;
|
|
421
|
+
changed = true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (SUPPORTED_CLAUDE_WORKSPACE_PHASES.has('UserPromptSubmit') && !existingPromptEntry) {
|
|
425
|
+
promptSubmit.push({
|
|
426
|
+
hooks: [
|
|
427
|
+
{
|
|
428
|
+
type: 'command',
|
|
429
|
+
command: CLAUDE_PROMPT_COMMAND,
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
});
|
|
433
|
+
config.hooks.UserPromptSubmit = promptSubmit;
|
|
434
|
+
changed = true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!changed && existsSync(filePath)) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
442
|
+
writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function removeManagedClaudeSettingsJson(filePath) {
|
|
447
|
+
if (!existsSync(filePath)) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let config;
|
|
452
|
+
try {
|
|
453
|
+
config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
454
|
+
} catch {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!config || typeof config !== 'object' || Array.isArray(config) || !config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let changed = false;
|
|
463
|
+
|
|
464
|
+
if (Array.isArray(config.hooks.SessionStart)) {
|
|
465
|
+
const nextSessionStart = config.hooks.SessionStart
|
|
466
|
+
.map(entry => {
|
|
467
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
468
|
+
return entry;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const filteredHooks = entry.hooks.filter(hook =>
|
|
472
|
+
!(hook?.type === 'command' && hook?.command === CLAUDE_HOOK_COMMAND)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (filteredHooks.length !== entry.hooks.length) {
|
|
476
|
+
changed = true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (filteredHooks.length === 0) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { ...entry, hooks: filteredHooks };
|
|
484
|
+
})
|
|
485
|
+
.filter(Boolean);
|
|
486
|
+
|
|
487
|
+
if (nextSessionStart.length > 0) {
|
|
488
|
+
config.hooks.SessionStart = nextSessionStart;
|
|
489
|
+
} else if (config.hooks.SessionStart.length > 0) {
|
|
490
|
+
delete config.hooks.SessionStart;
|
|
491
|
+
changed = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (Array.isArray(config.hooks.UserPromptSubmit)) {
|
|
496
|
+
const nextPromptSubmit = config.hooks.UserPromptSubmit
|
|
497
|
+
.map(entry => {
|
|
498
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
499
|
+
return entry;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const filteredHooks = entry.hooks.filter(hook =>
|
|
503
|
+
!(hook?.type === 'command' && hook?.command === CLAUDE_PROMPT_COMMAND)
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (filteredHooks.length !== entry.hooks.length) {
|
|
507
|
+
changed = true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (filteredHooks.length === 0) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return { ...entry, hooks: filteredHooks };
|
|
515
|
+
})
|
|
516
|
+
.filter(Boolean);
|
|
517
|
+
|
|
518
|
+
if (nextPromptSubmit.length > 0) {
|
|
519
|
+
config.hooks.UserPromptSubmit = nextPromptSubmit;
|
|
520
|
+
} else if (config.hooks.UserPromptSubmit.length > 0) {
|
|
521
|
+
delete config.hooks.UserPromptSubmit;
|
|
522
|
+
changed = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!changed) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (Object.keys(config.hooks).length === 0) {
|
|
531
|
+
delete config.hooks;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (isManagedJsonShell(config, [])) {
|
|
535
|
+
rmSync(filePath, { force: true });
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function removeManagedHooksJson(filePath) {
|
|
544
|
+
if (!existsSync(filePath)) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let config;
|
|
549
|
+
try {
|
|
550
|
+
config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
551
|
+
} catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let changed = false;
|
|
560
|
+
|
|
561
|
+
if (Array.isArray(config.hooks.SessionStart)) {
|
|
562
|
+
const nextSessionStart = config.hooks.SessionStart
|
|
563
|
+
.map(entry => {
|
|
564
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
565
|
+
return entry;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const filteredHooks = entry.hooks.filter(hook =>
|
|
569
|
+
!(hook?.type === 'command' && hook?.command === CODEX_HOOK_COMMAND)
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (filteredHooks.length !== entry.hooks.length) {
|
|
573
|
+
changed = true;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (filteredHooks.length === 0) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return { ...entry, hooks: filteredHooks };
|
|
581
|
+
})
|
|
582
|
+
.filter(Boolean);
|
|
583
|
+
|
|
584
|
+
if (nextSessionStart.length > 0) {
|
|
585
|
+
config.hooks.SessionStart = nextSessionStart;
|
|
586
|
+
} else if (config.hooks.SessionStart.length > 0) {
|
|
587
|
+
delete config.hooks.SessionStart;
|
|
588
|
+
changed = true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (Array.isArray(config.hooks.UserPromptSubmit)) {
|
|
593
|
+
const nextPromptSubmit = config.hooks.UserPromptSubmit
|
|
594
|
+
.map(entry => {
|
|
595
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
596
|
+
return entry;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const filteredHooks = entry.hooks.filter(hook =>
|
|
600
|
+
!(hook?.type === 'command' && hook?.command === CODEX_PROMPT_COMMAND)
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
if (filteredHooks.length !== entry.hooks.length) {
|
|
604
|
+
changed = true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (filteredHooks.length === 0) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return { ...entry, hooks: filteredHooks };
|
|
612
|
+
})
|
|
613
|
+
.filter(Boolean);
|
|
614
|
+
|
|
615
|
+
if (nextPromptSubmit.length > 0) {
|
|
616
|
+
config.hooks.UserPromptSubmit = nextPromptSubmit;
|
|
617
|
+
} else if (config.hooks.UserPromptSubmit.length > 0) {
|
|
618
|
+
delete config.hooks.UserPromptSubmit;
|
|
619
|
+
changed = true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!changed) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (Object.keys(config.hooks).length === 0) {
|
|
628
|
+
rmSync(filePath, { force: true });
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function removeManagedCodexConfig(filePath) {
|
|
637
|
+
if (!existsSync(filePath)) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
642
|
+
|
|
643
|
+
if (existing.includes(CONFIG_FILE_MARKER) || isLegacyHookOnlyConfig(existing)) {
|
|
644
|
+
rmSync(filePath, { force: true });
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let next = existing;
|
|
649
|
+
|
|
650
|
+
for (const server of CODEX_MANAGED_SERVERS) {
|
|
651
|
+
next = removeManagedBlock(next, serverBlockName(server.name));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
next = removeManagedBlock(next, FEATURES_BLOCK_NAME);
|
|
655
|
+
next = next.replace(
|
|
656
|
+
new RegExp(`^\\s*codex_hooks\\s*=\\s*true\\s*${escapeRegExp(HOOK_LINE_MARKER)}\\s*$\\n?`, 'm'),
|
|
657
|
+
''
|
|
658
|
+
);
|
|
659
|
+
next = next.replace(
|
|
660
|
+
new RegExp(`^\\s*codex_hooks\\s*=\\s*true\\s*${escapeRegExp(HOOK_LINE_REPLACED_MARKER)}\\s*$\\n?`, 'm'),
|
|
661
|
+
'codex_hooks = false\n'
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
next = tidyToml(next);
|
|
665
|
+
|
|
666
|
+
if (next === existing) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (next.trim() === '') {
|
|
671
|
+
rmSync(filePath, { force: true });
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
writeFileSync(filePath, next);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function ensureManagedServerBlocks(content) {
|
|
680
|
+
let next = content;
|
|
681
|
+
|
|
682
|
+
for (const server of CODEX_MANAGED_SERVERS) {
|
|
683
|
+
if (hasTomlSection(next, `mcp_servers.${server.name}`)) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
next = `${next.trimEnd()}\n\n${buildManagedBlock(serverBlockName(server.name), server.lines)}\n`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return tidyToml(next);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function ensureCodexHookEnabled(content) {
|
|
694
|
+
if (/^\s*codex_hooks\s*=\s*true\b/m.test(content)) {
|
|
695
|
+
return content;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (/^\s*codex_hooks\s*=\s*false\b/m.test(content)) {
|
|
699
|
+
return content.replace(
|
|
700
|
+
/^\s*codex_hooks\s*=\s*false\b.*$/m,
|
|
701
|
+
`codex_hooks = true ${HOOK_LINE_REPLACED_MARKER}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const featuresHeaderMatch = content.match(/^\[features\]\s*$/m);
|
|
706
|
+
if (featuresHeaderMatch && featuresHeaderMatch.index !== undefined) {
|
|
707
|
+
const insertAt = featuresHeaderMatch.index + featuresHeaderMatch[0].length;
|
|
708
|
+
return `${content.slice(0, insertAt)}\ncodex_hooks = true ${HOOK_LINE_MARKER}${content.slice(insertAt)}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return tidyToml(`${content.trimEnd()}\n\n${buildManagedBlock(FEATURES_BLOCK_NAME, ['[features]', 'codex_hooks = true'])}\n`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function buildManagedConfigFile() {
|
|
715
|
+
const lines = [
|
|
716
|
+
CONFIG_FILE_MARKER,
|
|
717
|
+
'#:schema https://developers.openai.com/codex/config-schema.json',
|
|
718
|
+
'',
|
|
719
|
+
'approval_policy = "on-request"',
|
|
720
|
+
'sandbox_mode = "workspace-write"',
|
|
721
|
+
'web_search = "live"',
|
|
722
|
+
'persistent_instructions = true',
|
|
723
|
+
];
|
|
724
|
+
|
|
725
|
+
for (const server of CODEX_MANAGED_SERVERS) {
|
|
726
|
+
lines.push('', ...buildManagedBlockLines(serverBlockName(server.name), server.lines));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
lines.push('', ...buildManagedBlockLines(FEATURES_BLOCK_NAME, ['[features]', 'codex_hooks = true']));
|
|
730
|
+
return `${lines.join('\n')}\n`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function buildManagedBlock(name, lines) {
|
|
734
|
+
return `${buildManagedBlockLines(name, lines).join('\n')}`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function buildManagedBlockLines(name, lines) {
|
|
738
|
+
return [`# aw-managed-start: ${name}`, ...lines, `# aw-managed-end: ${name}`];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function removeManagedBlock(content, name) {
|
|
742
|
+
const pattern = new RegExp(
|
|
743
|
+
`\\n?# aw-managed-start: ${escapeRegExp(name)}\\n[\\s\\S]*?\\n# aw-managed-end: ${escapeRegExp(name)}\\n?`,
|
|
744
|
+
'g'
|
|
745
|
+
);
|
|
746
|
+
return content.replace(pattern, '\n');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function ensureManagedScript(filePath, content, marker) {
|
|
750
|
+
if (existsSync(filePath)) {
|
|
751
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
752
|
+
if (existing === content) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
if (!existing.includes(marker)) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
761
|
+
writeFileSync(filePath, content);
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function removeManagedScript(filePath, marker) {
|
|
766
|
+
if (!existsSync(filePath)) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
771
|
+
if (!existing.includes(marker)) {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
rmSync(filePath, { force: true });
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function hasTomlSection(content, sectionName) {
|
|
780
|
+
const pattern = new RegExp(`^\\[${escapeRegExp(sectionName)}\\]\\s*$`, 'm');
|
|
781
|
+
return pattern.test(content);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function tidyToml(content) {
|
|
785
|
+
const normalized = content
|
|
786
|
+
.replace(/\r\n/g, '\n')
|
|
787
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
788
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
789
|
+
.trim();
|
|
790
|
+
return normalized ? `${normalized}\n` : '';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function serverBlockName(name) {
|
|
794
|
+
return `mcp_servers.${name}`;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function pruneEmptyDir(dirPath) {
|
|
798
|
+
if (!existsSync(dirPath)) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
if (readFileSync(join(dirPath, '.keep'))) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
} catch { /* no sentinel */ }
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
if (dirname(dirPath) !== dirPath && existsSync(dirPath) && readdirSync(dirPath).length === 0) {
|
|
810
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
811
|
+
}
|
|
812
|
+
} catch { /* best effort */ }
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function isLegacyHookOnlyConfig(content) {
|
|
816
|
+
const trimmed = content.trim();
|
|
817
|
+
return trimmed === '[features]\ncodex_hooks = true';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function escapeRegExp(value) {
|
|
821
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function isAwSessionStartCommand(command) {
|
|
825
|
+
return typeof command === 'string' && (
|
|
826
|
+
command.includes('hooks/aw-session-start') ||
|
|
827
|
+
command.includes('hooks/session-start') ||
|
|
828
|
+
(/run-hook\.cmd/.test(command) && /session-start/.test(command))
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function isAwPromptReminderCommand(command) {
|
|
833
|
+
return typeof command === 'string' && command.includes('scripts/hooks/session-start-rules-context.sh');
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function isManagedJsonShell(config, allowedKeys) {
|
|
837
|
+
const keys = Object.keys(config);
|
|
838
|
+
return keys.every(key => allowedKeys.includes(key));
|
|
839
|
+
}
|