@ghl-ai/aw 0.1.37-beta.52 → 0.1.37-beta.53

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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/startup.mjs +470 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.37-beta.52",
3
+ "version": "0.1.37-beta.53",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": "bin.js",
@@ -25,6 +25,7 @@
25
25
  "apply.mjs",
26
26
  "update.mjs",
27
27
  "hooks.mjs",
28
+ "startup.mjs",
28
29
  "ecc.mjs",
29
30
  "render-rules.mjs"
30
31
  ],
package/startup.mjs ADDED
@@ -0,0 +1,470 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ const STARTUP_PREFS_FILENAME = 'startup-preferences.json';
6
+ const ENABLED_MODE = 'enabled';
7
+ const DISABLED_MODE = 'disabled';
8
+
9
+ const CLAUDE_DISABLE_DESCRIPTION = 'AW-managed override: disable automatic AW session routing';
10
+ const CODEX_HOOK_MATCHER = 'startup|resume';
11
+ const CODEX_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-session-start.sh"';
12
+ const CODEX_HOOK_STATUS = 'Loading AW router';
13
+ const CODEX_HOOK_MARKER = '# aw-managed: codex-global-session-start';
14
+ const CURSOR_SESSION_START_COMMAND = 'node .cursor/hooks/session-start.js';
15
+ const CURSOR_SESSION_START_DESCRIPTION = 'Load previous context and detect environment';
16
+ const REPO_CURSOR_SESSION_START_COMMAND = 'bash "$(git rev-parse --show-toplevel)/hooks/aw-session-start"';
17
+ const REPO_CLAUDE_SESSION_START_COMMAND = '"$CLAUDE_PROJECT_DIR"/hooks/aw-session-start';
18
+
19
+ const CODEX_HOOK_SCRIPT = `#!/usr/bin/env bash
20
+ ${CODEX_HOOK_MARKER}
21
+ set -euo pipefail
22
+
23
+ TARGETS=(
24
+ "\$HOME/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"
25
+ "\$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"
26
+ )
27
+
28
+ for target in "\${TARGETS[@]}"; do
29
+ if [[ -f "\$target" ]]; then
30
+ exec bash "\$target"
31
+ fi
32
+ done
33
+
34
+ CONTEXT="# AW Session Context
35
+
36
+ WARNING: AW using-aw-skills hook not found in ~/.aw_registry. Run aw init or aw pull platform."
37
+
38
+ JSON_CONTEXT=$(printf '%s' "\$CONTEXT" | python3 -c 'import json, sys; print(json.dumps(sys.stdin.read()))')
39
+
40
+ echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"SessionStart\\",\\"additionalContext\\":\${JSON_CONTEXT}}}"
41
+ `;
42
+
43
+ function startupPrefsPath(homeDir = homedir()) {
44
+ return join(homeDir, '.aw', STARTUP_PREFS_FILENAME);
45
+ }
46
+
47
+ function codexConfigPath(homeDir = homedir()) {
48
+ return join(homeDir, '.codex', 'config.toml');
49
+ }
50
+
51
+ function codexHookScriptPath(homeDir = homedir()) {
52
+ return join(homeDir, '.codex', 'hooks', 'aw-session-start.sh');
53
+ }
54
+
55
+ function readJson(filePath, fallback = {}) {
56
+ if (!existsSync(filePath)) return fallback;
57
+ try {
58
+ return JSON.parse(readFileSync(filePath, 'utf8'));
59
+ } catch {
60
+ return fallback;
61
+ }
62
+ }
63
+
64
+ function writeJson(filePath, value) {
65
+ mkdirSync(dirname(filePath), { recursive: true });
66
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
67
+ }
68
+
69
+ function isObject(value) {
70
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
71
+ }
72
+
73
+ function isEmptyObject(value) {
74
+ return isObject(value) && Object.keys(value).length === 0;
75
+ }
76
+
77
+ function pruneEmptySettingsFile(filePath, config) {
78
+ if (!existsSync(filePath)) return false;
79
+ if (!isEmptyObject(config)) return false;
80
+ rmSync(filePath, { force: true });
81
+ return true;
82
+ }
83
+
84
+ function ensureHooksObject(config) {
85
+ if (!isObject(config.hooks)) {
86
+ config.hooks = {};
87
+ }
88
+ }
89
+
90
+ function isManagedClaudeSessionStartOverride(entry) {
91
+ return entry?.description === CLAUDE_DISABLE_DESCRIPTION;
92
+ }
93
+
94
+ function isManagedCodexSessionStartEntry(entry) {
95
+ return entry?.matcher === CODEX_HOOK_MATCHER
96
+ && Array.isArray(entry?.hooks)
97
+ && entry.hooks.some(hook =>
98
+ hook?.type === 'command'
99
+ && hook?.command === CODEX_HOOK_COMMAND
100
+ );
101
+ }
102
+
103
+ function isLegacyCodexSessionStartEntry(entry) {
104
+ return Array.isArray(entry?.hooks)
105
+ && entry.hooks.some(hook =>
106
+ hook?.type === 'command'
107
+ && typeof hook?.command === 'string'
108
+ && hook.command.includes('.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh')
109
+ );
110
+ }
111
+
112
+ function disableClaudeStartup(homeDir = homedir()) {
113
+ const settingsPath = join(homeDir, '.claude', 'settings.json');
114
+ const config = readJson(settingsPath, {});
115
+ ensureHooksObject(config);
116
+
117
+ const current = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
118
+ if (current.some(isManagedClaudeSessionStartOverride)) {
119
+ return [];
120
+ }
121
+
122
+ config.hooks.SessionStart = [
123
+ {
124
+ matcher: '*',
125
+ hooks: [],
126
+ description: CLAUDE_DISABLE_DESCRIPTION,
127
+ },
128
+ ...current,
129
+ ];
130
+
131
+ writeJson(settingsPath, config);
132
+ return [settingsPath];
133
+ }
134
+
135
+ function enableClaudeStartup(homeDir = homedir()) {
136
+ const settingsPath = join(homeDir, '.claude', 'settings.json');
137
+ if (!existsSync(settingsPath)) return [];
138
+
139
+ const config = readJson(settingsPath, {});
140
+ if (!isObject(config.hooks) || !Array.isArray(config.hooks.SessionStart)) {
141
+ return [];
142
+ }
143
+
144
+ const filtered = config.hooks.SessionStart.filter(entry => !isManagedClaudeSessionStartOverride(entry));
145
+ if (filtered.length === config.hooks.SessionStart.length) {
146
+ return [];
147
+ }
148
+
149
+ if (filtered.length > 0) {
150
+ config.hooks.SessionStart = filtered;
151
+ } else {
152
+ delete config.hooks.SessionStart;
153
+ }
154
+
155
+ if (isEmptyObject(config.hooks)) {
156
+ delete config.hooks;
157
+ }
158
+
159
+ if (!pruneEmptySettingsFile(settingsPath, config)) {
160
+ writeJson(settingsPath, config);
161
+ }
162
+
163
+ return [settingsPath];
164
+ }
165
+
166
+ function hasCodexSessionStartScript(homeDir = homedir()) {
167
+ return existsSync(codexHookScriptPath(homeDir));
168
+ }
169
+
170
+ function hasCodexHooksEnabled(homeDir = homedir()) {
171
+ const configPath = codexConfigPath(homeDir);
172
+ if (!existsSync(configPath)) return false;
173
+
174
+ try {
175
+ return /^\s*codex_hooks\s*=\s*true\b/m.test(readFileSync(configPath, 'utf8'));
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function ensureCodexHookEnabled(content) {
182
+ if (/^\s*codex_hooks\s*=\s*true\b/m.test(content)) {
183
+ return content;
184
+ }
185
+
186
+ if (/^\s*codex_hooks\s*=\s*false\b/m.test(content)) {
187
+ return content.replace(/^\s*codex_hooks\s*=\s*false\b.*$/m, 'codex_hooks = true');
188
+ }
189
+
190
+ const featuresHeaderMatch = content.match(/^\[features\]\s*$/m);
191
+ if (featuresHeaderMatch && featuresHeaderMatch.index !== undefined) {
192
+ const insertAt = featuresHeaderMatch.index + featuresHeaderMatch[0].length;
193
+ return `${content.slice(0, insertAt)}\ncodex_hooks = true${content.slice(insertAt)}`;
194
+ }
195
+
196
+ const trimmed = content.trimEnd();
197
+ if (trimmed.length === 0) {
198
+ return '[features]\ncodex_hooks = true\n';
199
+ }
200
+
201
+ return `${trimmed}\n\n[features]\ncodex_hooks = true\n`;
202
+ }
203
+
204
+ function enableCodexStartup(homeDir = homedir()) {
205
+ const updatedFiles = [];
206
+ const configPath = codexConfigPath(homeDir);
207
+ const currentConfig = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
208
+ const nextConfig = ensureCodexHookEnabled(currentConfig);
209
+
210
+ if (!existsSync(configPath) || nextConfig !== currentConfig) {
211
+ mkdirSync(dirname(configPath), { recursive: true });
212
+ writeFileSync(configPath, nextConfig);
213
+ updatedFiles.push(configPath);
214
+ }
215
+
216
+ const hookScriptPath = codexHookScriptPath(homeDir);
217
+ const currentHookScript = existsSync(hookScriptPath) ? readFileSync(hookScriptPath, 'utf8') : '';
218
+ if (!existsSync(hookScriptPath) || currentHookScript !== CODEX_HOOK_SCRIPT) {
219
+ mkdirSync(dirname(hookScriptPath), { recursive: true });
220
+ writeFileSync(hookScriptPath, CODEX_HOOK_SCRIPT);
221
+ updatedFiles.push(hookScriptPath);
222
+ }
223
+
224
+ const hooksPath = join(homeDir, '.codex', 'hooks.json');
225
+ const config = readJson(hooksPath, {});
226
+
227
+ if (!isObject(config.hooks)) {
228
+ config.hooks = {};
229
+ }
230
+
231
+ const current = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
232
+ const cleaned = current.filter(entry => !isLegacyCodexSessionStartEntry(entry) && !isManagedCodexSessionStartEntry(entry));
233
+ const hasManagedEntry = current.some(isManagedCodexSessionStartEntry);
234
+
235
+ if (!hasManagedEntry || cleaned.length !== current.length) {
236
+ config.hooks.SessionStart = [
237
+ ...cleaned,
238
+ {
239
+ matcher: CODEX_HOOK_MATCHER,
240
+ hooks: [
241
+ {
242
+ type: 'command',
243
+ command: CODEX_HOOK_COMMAND,
244
+ statusMessage: CODEX_HOOK_STATUS,
245
+ },
246
+ ],
247
+ },
248
+ ];
249
+ writeJson(hooksPath, config);
250
+ updatedFiles.push(hooksPath);
251
+ }
252
+
253
+ return updatedFiles;
254
+ }
255
+
256
+ function disableCodexStartup(homeDir = homedir()) {
257
+ const updatedFiles = [];
258
+ const hookScriptPath = codexHookScriptPath(homeDir);
259
+ if (existsSync(hookScriptPath)) {
260
+ try {
261
+ const currentHookScript = readFileSync(hookScriptPath, 'utf8');
262
+ if (currentHookScript.includes(CODEX_HOOK_MARKER)) {
263
+ rmSync(hookScriptPath, { force: true });
264
+ updatedFiles.push(hookScriptPath);
265
+ }
266
+ } catch {
267
+ /* best effort */
268
+ }
269
+ }
270
+
271
+ const hooksPath = join(homeDir, '.codex', 'hooks.json');
272
+ if (!existsSync(hooksPath)) return updatedFiles;
273
+
274
+ const config = readJson(hooksPath, {});
275
+ if (!isObject(config.hooks) || !Array.isArray(config.hooks.SessionStart)) {
276
+ return updatedFiles;
277
+ }
278
+
279
+ const filtered = config.hooks.SessionStart.filter(entry => !isManagedCodexSessionStartEntry(entry) && !isLegacyCodexSessionStartEntry(entry));
280
+ if (filtered.length === config.hooks.SessionStart.length) {
281
+ return updatedFiles;
282
+ }
283
+
284
+ if (filtered.length > 0) {
285
+ config.hooks.SessionStart = filtered;
286
+ } else {
287
+ delete config.hooks.SessionStart;
288
+ }
289
+
290
+ if (isEmptyObject(config.hooks)) {
291
+ delete config.hooks;
292
+ }
293
+
294
+ if (!pruneEmptySettingsFile(hooksPath, config)) {
295
+ writeJson(hooksPath, config);
296
+ }
297
+
298
+ updatedFiles.push(hooksPath);
299
+ return updatedFiles;
300
+ }
301
+
302
+ function isManagedCursorSessionStartEntry(entry) {
303
+ const command = String(entry?.command || '');
304
+ return command === CURSOR_SESSION_START_COMMAND || command.endsWith('.cursor/hooks/session-start.js');
305
+ }
306
+
307
+ function hasCursorSessionStartScript(homeDir = homedir()) {
308
+ return existsSync(join(homeDir, '.cursor', 'hooks', 'session-start.js'));
309
+ }
310
+
311
+ function disableCursorStartup(homeDir = homedir()) {
312
+ const hooksPath = join(homeDir, '.cursor', 'hooks.json');
313
+ if (!existsSync(hooksPath)) return [];
314
+
315
+ const config = readJson(hooksPath, {});
316
+ if (!isObject(config.hooks) || !Array.isArray(config.hooks.sessionStart)) {
317
+ return [];
318
+ }
319
+
320
+ const filtered = config.hooks.sessionStart.filter(entry => !isManagedCursorSessionStartEntry(entry));
321
+ if (filtered.length === config.hooks.sessionStart.length) {
322
+ return [];
323
+ }
324
+
325
+ if (filtered.length > 0) {
326
+ config.hooks.sessionStart = filtered;
327
+ } else {
328
+ delete config.hooks.sessionStart;
329
+ }
330
+
331
+ if (isEmptyObject(config.hooks)) {
332
+ delete config.hooks;
333
+ }
334
+
335
+ if (config.version === undefined) {
336
+ config.version = 1;
337
+ }
338
+
339
+ if (!pruneEmptySettingsFile(hooksPath, config)) {
340
+ writeJson(hooksPath, config);
341
+ }
342
+
343
+ return [hooksPath];
344
+ }
345
+
346
+ function enableCursorStartup(homeDir = homedir()) {
347
+ if (!hasCursorSessionStartScript(homeDir)) return [];
348
+
349
+ const hooksPath = join(homeDir, '.cursor', 'hooks.json');
350
+ const config = readJson(hooksPath, {});
351
+
352
+ if (!isObject(config.hooks)) {
353
+ config.hooks = {};
354
+ }
355
+
356
+ const current = Array.isArray(config.hooks.sessionStart) ? config.hooks.sessionStart : [];
357
+ if (current.length > 0) {
358
+ return [];
359
+ }
360
+
361
+ config.version = Number.isInteger(config.version) ? config.version : 1;
362
+ config.hooks.sessionStart = [
363
+ {
364
+ command: CURSOR_SESSION_START_COMMAND,
365
+ event: 'sessionStart',
366
+ description: CURSOR_SESSION_START_DESCRIPTION,
367
+ },
368
+ ];
369
+
370
+ writeJson(hooksPath, config);
371
+ return [hooksPath];
372
+ }
373
+
374
+ export function loadStartupPreferences(homeDir = homedir()) {
375
+ const prefs = readJson(startupPrefsPath(homeDir), {});
376
+ return {
377
+ mode: prefs.mode === DISABLED_MODE ? DISABLED_MODE : ENABLED_MODE,
378
+ updatedAt: typeof prefs.updatedAt === 'string' ? prefs.updatedAt : null,
379
+ };
380
+ }
381
+
382
+ export function saveStartupPreferences(mode, homeDir = homedir()) {
383
+ const normalizedMode = mode === DISABLED_MODE ? DISABLED_MODE : ENABLED_MODE;
384
+ const next = {
385
+ mode: normalizedMode,
386
+ updatedAt: new Date().toISOString(),
387
+ };
388
+ writeJson(startupPrefsPath(homeDir), next);
389
+ return next;
390
+ }
391
+
392
+ export function clearStartupPreferences(homeDir = homedir()) {
393
+ const prefsPath = startupPrefsPath(homeDir);
394
+ if (!existsSync(prefsPath)) return false;
395
+ rmSync(prefsPath, { force: true });
396
+ return true;
397
+ }
398
+
399
+ export function applyGlobalStartupMode(mode, homeDir = homedir()) {
400
+ const normalizedMode = mode === DISABLED_MODE ? DISABLED_MODE : ENABLED_MODE;
401
+ const updatedFiles = [];
402
+
403
+ if (normalizedMode === DISABLED_MODE) {
404
+ updatedFiles.push(...disableClaudeStartup(homeDir));
405
+ updatedFiles.push(...disableCodexStartup(homeDir));
406
+ updatedFiles.push(...disableCursorStartup(homeDir));
407
+ } else {
408
+ updatedFiles.push(...enableClaudeStartup(homeDir));
409
+ updatedFiles.push(...enableCodexStartup(homeDir));
410
+ updatedFiles.push(...enableCursorStartup(homeDir));
411
+ }
412
+
413
+ return [...new Set(updatedFiles)];
414
+ }
415
+
416
+ export function applyStoredStartupPreferences(homeDir = homedir()) {
417
+ return applyGlobalStartupMode(loadStartupPreferences(homeDir).mode, homeDir);
418
+ }
419
+
420
+ export function getStartupStatus(homeDir = homedir()) {
421
+ const prefs = loadStartupPreferences(homeDir);
422
+ const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');
423
+ const codexHooksPath = join(homeDir, '.codex', 'hooks.json');
424
+ const cursorHooksPath = join(homeDir, '.cursor', 'hooks.json');
425
+ const claudeSettings = readJson(claudeSettingsPath, {});
426
+ const codexHooks = readJson(codexHooksPath, {});
427
+ const cursorHooks = readJson(cursorHooksPath, {});
428
+
429
+ return {
430
+ ...prefs,
431
+ preferencesPath: startupPrefsPath(homeDir),
432
+ claudeDisabled: Array.isArray(claudeSettings?.hooks?.SessionStart) &&
433
+ claudeSettings.hooks.SessionStart.some(isManagedClaudeSessionStartOverride),
434
+ codexHooksEnabled: hasCodexHooksEnabled(homeDir),
435
+ codexSessionStartPresent: Array.isArray(codexHooks?.hooks?.SessionStart) &&
436
+ codexHooks.hooks.SessionStart.some(isManagedCodexSessionStartEntry),
437
+ codexSessionStartScriptInstalled: hasCodexSessionStartScript(homeDir),
438
+ cursorSessionStartPresent: Array.isArray(cursorHooks?.hooks?.sessionStart) &&
439
+ cursorHooks.hooks.sessionStart.length > 0,
440
+ cursorSessionStartScriptInstalled: hasCursorSessionStartScript(homeDir),
441
+ };
442
+ }
443
+
444
+ export function hasLegacyRepoStartupDefaults(cwd) {
445
+ const codexHooksPath = join(cwd, '.codex', 'hooks.json');
446
+ const cursorHooksPath = join(cwd, '.cursor', 'hooks.json');
447
+ const claudeSettingsPath = join(cwd, '.claude', 'settings.json');
448
+ const wrapperPath = join(cwd, 'hooks', 'aw-session-start');
449
+
450
+ const codexHooks = readJson(codexHooksPath, {});
451
+ const cursorHooks = readJson(cursorHooksPath, {});
452
+ const claudeSettings = readJson(claudeSettingsPath, {});
453
+
454
+ const codexSessionStart = Array.isArray(codexHooks?.hooks?.SessionStart)
455
+ && codexHooks.hooks.SessionStart.some(entry =>
456
+ Array.isArray(entry?.hooks)
457
+ && entry.hooks.some(hook => String(hook?.command || '').includes('hooks/aw-session-start'))
458
+ );
459
+
460
+ const cursorSessionStart = Array.isArray(cursorHooks?.hooks?.sessionStart)
461
+ && cursorHooks.hooks.sessionStart.some(entry => String(entry?.command || '') === REPO_CURSOR_SESSION_START_COMMAND);
462
+
463
+ const claudeSessionStart = Array.isArray(claudeSettings?.hooks?.SessionStart)
464
+ && claudeSettings.hooks.SessionStart.some(entry =>
465
+ Array.isArray(entry?.hooks)
466
+ && entry.hooks.some(hook => String(hook?.command || '') === REPO_CLAUDE_SESSION_START_COMMAND)
467
+ );
468
+
469
+ return existsSync(wrapperPath) || codexSessionStart || cursorSessionStart || claudeSessionStart;
470
+ }