@ghl-ai/aw 0.1.42-beta.26 → 0.1.42-beta.28
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/commands/init.mjs +8 -0
- package/ecc.mjs +8 -3
- package/hook-cleanup.mjs +301 -0
- package/package.json +2 -1
package/commands/init.mjs
CHANGED
|
@@ -339,12 +339,16 @@ export async function initCommand(args) {
|
|
|
339
339
|
if (oldManifest) pruneStaleHooks(oldManifest);
|
|
340
340
|
|
|
341
341
|
await installAwEcc(cwd, { silent });
|
|
342
|
+
|
|
343
|
+
const setupSpinner = silent ? null : fmt.spinner();
|
|
344
|
+
if (setupSpinner) setupSpinner.start('Configuring hooks and IDE integration...');
|
|
342
345
|
ensureAwRuntimeHook(HOME);
|
|
343
346
|
syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
|
|
344
347
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
345
348
|
applyStoredStartupPreferences(HOME);
|
|
346
349
|
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
347
350
|
installGlobalHooks();
|
|
351
|
+
if (setupSpinner) setupSpinner.stop('Hooks and IDE integration configured');
|
|
348
352
|
|
|
349
353
|
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
350
354
|
if (cwd !== HOME) {
|
|
@@ -482,6 +486,9 @@ export async function initCommand(args) {
|
|
|
482
486
|
if (oldManifestFresh) pruneStaleHooks(oldManifestFresh);
|
|
483
487
|
|
|
484
488
|
await installAwEcc(cwd, { silent });
|
|
489
|
+
|
|
490
|
+
const setupSpinnerFresh = silent ? null : fmt.spinner();
|
|
491
|
+
if (setupSpinnerFresh) setupSpinnerFresh.start('Configuring hooks and IDE integration...');
|
|
485
492
|
ensureAwRuntimeHook(HOME);
|
|
486
493
|
|
|
487
494
|
// Parallel batch B: post-ECC setup (instructions and MCP are independent)
|
|
@@ -494,6 +501,7 @@ export async function initCommand(args) {
|
|
|
494
501
|
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
495
502
|
const hooksInstalled = installGlobalHooks();
|
|
496
503
|
installIdeTasks();
|
|
504
|
+
if (setupSpinnerFresh) setupSpinnerFresh.stop('Hooks and IDE integration configured');
|
|
497
505
|
|
|
498
506
|
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
|
|
499
507
|
if (cwd !== HOME) {
|
package/ecc.mjs
CHANGED
|
@@ -259,13 +259,16 @@ export async function installAwEcc(
|
|
|
259
259
|
{ targets = ["cursor", "claude", "codex"], silent = false } = {},
|
|
260
260
|
) {
|
|
261
261
|
if (process.env.AW_NO_ECC === '1') return;
|
|
262
|
-
if (!silent) fmt.logStep("Installing aw-ecc engine...");
|
|
263
262
|
|
|
264
263
|
const repoDir = eccDir();
|
|
265
264
|
const home = homedir();
|
|
266
265
|
|
|
266
|
+
const eccSpinner = silent ? null : fmt.spinner();
|
|
267
|
+
|
|
267
268
|
try {
|
|
269
|
+
if (eccSpinner) eccSpinner.start('Cloning aw-ecc engine...');
|
|
268
270
|
cloneOrUpdate(AW_ECC_TAG, repoDir);
|
|
271
|
+
if (eccSpinner) eccSpinner.message('Installing aw-ecc dependencies...');
|
|
269
272
|
|
|
270
273
|
// Claude Code: plugin install via marketplace CLI (proper agent dispatch)
|
|
271
274
|
if (targets.includes("claude")) {
|
|
@@ -291,6 +294,7 @@ export async function installAwEcc(
|
|
|
291
294
|
run(`node "${generateHooksScript}"`, { cwd: repoDir });
|
|
292
295
|
} catch { /* best effort — older engine versions may not have this script */ }
|
|
293
296
|
}
|
|
297
|
+
if (eccSpinner) eccSpinner.message('Applying aw-ecc to IDE targets...');
|
|
294
298
|
// Each target writes to disjoint paths (~/.claude/, ~/.cursor/, ~/.codex/) — safe to parallelize.
|
|
295
299
|
await Promise.all(fileCopyTargets.map(async (target) => {
|
|
296
300
|
try {
|
|
@@ -319,9 +323,10 @@ export async function installAwEcc(
|
|
|
319
323
|
|
|
320
324
|
applyStoredStartupPreferences();
|
|
321
325
|
|
|
322
|
-
if (
|
|
326
|
+
if (eccSpinner) eccSpinner.stop('aw-ecc engine installed');
|
|
323
327
|
} catch (err) {
|
|
324
|
-
if (
|
|
328
|
+
if (eccSpinner) eccSpinner.stop(chalk.yellow(`aw-ecc install failed: ${err.message}`));
|
|
329
|
+
else if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
|
|
325
330
|
}
|
|
326
331
|
}
|
|
327
332
|
|
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.28",
|
|
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",
|