@ghl-ai/aw 0.1.42-beta.26 → 0.1.42-beta.27
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/hook-cleanup.mjs +301 -0
- package/package.json +2 -1
package/hook-cleanup.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// hook-cleanup.mjs — Hook cleanup manifest for safe version transitions.
|
|
2
|
+
//
|
|
3
|
+
// writeHookManifest() → snapshots all AW hook touchpoints to ~/.aw/hooks/manifest.json
|
|
4
|
+
// readHookManifest() → reads and validates the manifest
|
|
5
|
+
// pruneStaleHooks() → removes hook entries and runtime deps listed in a manifest
|
|
6
|
+
// removeHookManifest() → deletes the manifest file
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
existsSync, mkdirSync, readFileSync, readdirSync,
|
|
10
|
+
rmSync, writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
|
|
15
|
+
const SCHEMA_VERSION = 'aw-hooks.v1';
|
|
16
|
+
const HOME = homedir();
|
|
17
|
+
const MANIFEST_PATH = join(HOME, '.aw', 'hooks', 'manifest.json');
|
|
18
|
+
|
|
19
|
+
// ── Patterns matching AW-managed hook entries ────────────────────────────
|
|
20
|
+
|
|
21
|
+
function isManagedClaudeEntry(entry) {
|
|
22
|
+
if (entry?.description === 'AW usage telemetry') return true;
|
|
23
|
+
const cmds = Array.isArray(entry?.hooks) ? entry.hooks.map(h => h?.command || '') : [];
|
|
24
|
+
return cmds.some(c => c.includes('aw-usage-'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isManagedCursorEntry(entry) {
|
|
28
|
+
const cmd = String(entry?.command || '');
|
|
29
|
+
return cmd.includes('.cursor/hooks/') || cmd.includes('aw-ecc');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isManagedCodexEntry(entry) {
|
|
33
|
+
if (Array.isArray(entry?.hooks)) {
|
|
34
|
+
return entry.hooks.some(h => {
|
|
35
|
+
const cmd = String(h?.command || '');
|
|
36
|
+
return cmd.includes('.codex/hooks/') || cmd.includes('aw-ecc') || cmd.includes('aw-session-start');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Read helpers ─────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function readJson(filePath) {
|
|
45
|
+
if (!existsSync(filePath)) return null;
|
|
46
|
+
try { return JSON.parse(readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeJson(filePath, data) {
|
|
50
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
51
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Collect current hook state ───────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function collectClaudeTouchpoints() {
|
|
57
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
58
|
+
const hooksJsonPath = join(HOME, '.claude', 'hooks', 'hooks.json');
|
|
59
|
+
const settings = readJson(settingsPath);
|
|
60
|
+
const phases = [];
|
|
61
|
+
if (settings?.hooks) {
|
|
62
|
+
for (const phase of Object.keys(settings.hooks)) {
|
|
63
|
+
const entries = settings.hooks[phase];
|
|
64
|
+
if (Array.isArray(entries) && entries.some(isManagedClaudeEntry)) {
|
|
65
|
+
phases.push(phase);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
settingsPath,
|
|
71
|
+
hooksJsonPath: existsSync(hooksJsonPath) ? hooksJsonPath : null,
|
|
72
|
+
managedPhases: phases,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collectCursorTouchpoints() {
|
|
77
|
+
const hooksJsonPath = join(HOME, '.cursor', 'hooks.json');
|
|
78
|
+
const config = readJson(hooksJsonPath);
|
|
79
|
+
const phases = [];
|
|
80
|
+
if (config?.hooks) {
|
|
81
|
+
for (const phase of Object.keys(config.hooks)) {
|
|
82
|
+
const entries = config.hooks[phase];
|
|
83
|
+
if (Array.isArray(entries) && entries.some(isManagedCursorEntry)) {
|
|
84
|
+
phases.push(phase);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { hooksJsonPath, managedPhases: phases };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectCodexTouchpoints() {
|
|
92
|
+
const hooksJsonPath = join(HOME, '.codex', 'hooks.json');
|
|
93
|
+
const configPath = join(HOME, '.codex', 'config.toml');
|
|
94
|
+
const config = readJson(hooksJsonPath);
|
|
95
|
+
const phases = [];
|
|
96
|
+
if (config?.hooks) {
|
|
97
|
+
for (const phase of Object.keys(config.hooks)) {
|
|
98
|
+
const entries = config.hooks[phase];
|
|
99
|
+
if (Array.isArray(entries) && entries.some(isManagedCodexEntry)) {
|
|
100
|
+
phases.push(phase);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
configPath: existsSync(configPath) ? configPath : null,
|
|
106
|
+
hooksJsonPath,
|
|
107
|
+
managedPhases: phases,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collectGitTouchpoints() {
|
|
112
|
+
const hooksDir = join(HOME, '.aw', 'hooks');
|
|
113
|
+
const scripts = [];
|
|
114
|
+
if (existsSync(hooksDir)) {
|
|
115
|
+
try {
|
|
116
|
+
for (const entry of readdirSync(hooksDir)) {
|
|
117
|
+
// Exclude the manifest itself and hidden files
|
|
118
|
+
if (entry === 'manifest.json' || entry.startsWith('.')) continue;
|
|
119
|
+
scripts.push(entry);
|
|
120
|
+
}
|
|
121
|
+
} catch { /* best effort */ }
|
|
122
|
+
}
|
|
123
|
+
return { hooksDir, scripts };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function collectRuntimeDeps() {
|
|
127
|
+
const deps = [];
|
|
128
|
+
const hooksDir = join(HOME, '.aw-ecc', 'scripts', 'hooks');
|
|
129
|
+
const libDir = join(HOME, '.aw-ecc', 'scripts', 'lib');
|
|
130
|
+
|
|
131
|
+
if (existsSync(hooksDir)) {
|
|
132
|
+
try {
|
|
133
|
+
for (const entry of readdirSync(hooksDir)) {
|
|
134
|
+
if (entry.startsWith('aw-usage-')) {
|
|
135
|
+
deps.push(join(hooksDir, entry));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch { /* best effort */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const telemetryLib = join(libDir, 'aw-usage-telemetry.js');
|
|
142
|
+
if (existsSync(telemetryLib)) {
|
|
143
|
+
deps.push(telemetryLib);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return deps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Write a hook manifest capturing all current AW hook touchpoints.
|
|
153
|
+
* @param {{ eccVersion: string, awVersion: string }} opts
|
|
154
|
+
* @returns {object} the written manifest
|
|
155
|
+
*/
|
|
156
|
+
export function writeHookManifest({ eccVersion, awVersion }) {
|
|
157
|
+
const manifest = {
|
|
158
|
+
schemaVersion: SCHEMA_VERSION,
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
eccVersion,
|
|
161
|
+
awVersion,
|
|
162
|
+
touchpoints: {
|
|
163
|
+
claude: collectClaudeTouchpoints(),
|
|
164
|
+
cursor: collectCursorTouchpoints(),
|
|
165
|
+
codex: collectCodexTouchpoints(),
|
|
166
|
+
git: collectGitTouchpoints(),
|
|
167
|
+
},
|
|
168
|
+
runtimeDeps: collectRuntimeDeps(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
writeJson(MANIFEST_PATH, manifest);
|
|
172
|
+
return manifest;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read and validate the hook manifest.
|
|
177
|
+
* @returns {object|null} the manifest, or null if missing/invalid
|
|
178
|
+
*/
|
|
179
|
+
export function readHookManifest() {
|
|
180
|
+
const data = readJson(MANIFEST_PATH);
|
|
181
|
+
if (!data || data.schemaVersion !== SCHEMA_VERSION) return null;
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove hook entries and runtime deps listed in a manifest.
|
|
187
|
+
* Does NOT touch git hooks (handled by removeGlobalHooks).
|
|
188
|
+
* @param {object} manifest — from readHookManifest()
|
|
189
|
+
* @returns {number} count of items removed
|
|
190
|
+
*/
|
|
191
|
+
export function pruneStaleHooks(manifest) {
|
|
192
|
+
if (!manifest?.touchpoints) return 0;
|
|
193
|
+
let removed = 0;
|
|
194
|
+
|
|
195
|
+
// Claude: remove managed telemetry entries from settings.json
|
|
196
|
+
const claudeSettings = manifest.touchpoints.claude;
|
|
197
|
+
if (claudeSettings?.settingsPath && claudeSettings.managedPhases?.length > 0) {
|
|
198
|
+
const config = readJson(claudeSettings.settingsPath);
|
|
199
|
+
if (config?.hooks) {
|
|
200
|
+
let changed = false;
|
|
201
|
+
for (const phase of claudeSettings.managedPhases) {
|
|
202
|
+
const entries = config.hooks[phase];
|
|
203
|
+
if (!Array.isArray(entries)) continue;
|
|
204
|
+
const filtered = entries.filter(e => !isManagedClaudeEntry(e));
|
|
205
|
+
if (filtered.length !== entries.length) {
|
|
206
|
+
changed = true;
|
|
207
|
+
removed += entries.length - filtered.length;
|
|
208
|
+
if (filtered.length > 0) {
|
|
209
|
+
config.hooks[phase] = filtered;
|
|
210
|
+
} else {
|
|
211
|
+
delete config.hooks[phase];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (changed) {
|
|
216
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
217
|
+
writeJson(claudeSettings.settingsPath, config);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Cursor: remove managed entries from hooks.json
|
|
223
|
+
const cursorHooks = manifest.touchpoints.cursor;
|
|
224
|
+
if (cursorHooks?.hooksJsonPath && cursorHooks.managedPhases?.length > 0) {
|
|
225
|
+
const config = readJson(cursorHooks.hooksJsonPath);
|
|
226
|
+
if (config?.hooks) {
|
|
227
|
+
let changed = false;
|
|
228
|
+
for (const phase of cursorHooks.managedPhases) {
|
|
229
|
+
const entries = config.hooks[phase];
|
|
230
|
+
if (!Array.isArray(entries)) continue;
|
|
231
|
+
const filtered = entries.filter(e => !isManagedCursorEntry(e));
|
|
232
|
+
if (filtered.length !== entries.length) {
|
|
233
|
+
changed = true;
|
|
234
|
+
removed += entries.length - filtered.length;
|
|
235
|
+
if (filtered.length > 0) {
|
|
236
|
+
config.hooks[phase] = filtered;
|
|
237
|
+
} else {
|
|
238
|
+
delete config.hooks[phase];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (changed) {
|
|
243
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
244
|
+
writeJson(cursorHooks.hooksJsonPath, config);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Codex: remove managed entries from hooks.json
|
|
250
|
+
const codexHooks = manifest.touchpoints.codex;
|
|
251
|
+
if (codexHooks?.hooksJsonPath && codexHooks.managedPhases?.length > 0) {
|
|
252
|
+
const config = readJson(codexHooks.hooksJsonPath);
|
|
253
|
+
if (config?.hooks) {
|
|
254
|
+
let changed = false;
|
|
255
|
+
for (const phase of codexHooks.managedPhases) {
|
|
256
|
+
const entries = config.hooks[phase];
|
|
257
|
+
if (!Array.isArray(entries)) continue;
|
|
258
|
+
const filtered = entries.filter(e => !isManagedCodexEntry(e));
|
|
259
|
+
if (filtered.length !== entries.length) {
|
|
260
|
+
changed = true;
|
|
261
|
+
removed += entries.length - filtered.length;
|
|
262
|
+
if (filtered.length > 0) {
|
|
263
|
+
config.hooks[phase] = filtered;
|
|
264
|
+
} else {
|
|
265
|
+
delete config.hooks[phase];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (changed) {
|
|
270
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
271
|
+
writeJson(codexHooks.hooksJsonPath, config);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Runtime deps: remove aw-usage-* files
|
|
277
|
+
if (Array.isArray(manifest.runtimeDeps)) {
|
|
278
|
+
for (const dep of manifest.runtimeDeps) {
|
|
279
|
+
if (existsSync(dep)) {
|
|
280
|
+
try {
|
|
281
|
+
rmSync(dep, { force: true });
|
|
282
|
+
removed++;
|
|
283
|
+
} catch { /* best effort */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return removed;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Delete the manifest file.
|
|
293
|
+
*/
|
|
294
|
+
export function removeHookManifest() {
|
|
295
|
+
if (existsSync(MANIFEST_PATH)) {
|
|
296
|
+
rmSync(MANIFEST_PATH, { force: true });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Export for testing
|
|
301
|
+
export { MANIFEST_PATH, SCHEMA_VERSION };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.42-beta.
|
|
3
|
+
"version": "0.1.42-beta.27",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"slack-sim/",
|
|
27
27
|
"file-tree.mjs",
|
|
28
28
|
"apply.mjs",
|
|
29
|
+
"hook-cleanup.mjs",
|
|
29
30
|
"hook-manifest.mjs",
|
|
30
31
|
"update.mjs",
|
|
31
32
|
"hooks.mjs",
|