@ghl-ai/aw 0.1.55 → 0.1.56-beta.0

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.
@@ -0,0 +1,303 @@
1
+ // install-aw-usage-hooks.mjs — Self-sufficient producer-hook install.
2
+ //
3
+ // Why this exists:
4
+ // The aw-usage-*.js producer hooks (which emit telemetry events to the
5
+ // live cloud DB) used to live in aw-ecc and required a separate install +
6
+ // manual wiring into ~/.claude/settings.json. Devs running
7
+ // npm install -g @ghl-ai/aw && aw init
8
+ // would NOT get telemetry flowing — there was a silent gap in the
9
+ // installer. This module bundles the producer scripts into @ghl-ai/aw
10
+ // and writes them + the hook wiring during `aw init` so that
11
+ // step-1-then-step-2 is genuinely sufficient.
12
+ //
13
+ // What it does on `aw init`:
14
+ // 1. Copies bundled aw-usage scripts (libs/aw/hooks/aw-usage/*.js) into
15
+ // ~/.claude/scripts/hooks/
16
+ // 2. Copies the supporting lib files (aw-usage-telemetry.js, aw-pricing.js)
17
+ // into ~/.claude/scripts/lib/
18
+ // 3. Non-destructively MERGES five hook phases into ~/.claude/settings.json:
19
+ // SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop
20
+ // (Each phase array gets ONE new entry appended, with `description` so
21
+ // it can be identified + re-applied on future runs.)
22
+ // 4. Ensures ~/.aw/telemetry/config.json exists with machine_id + enabled
23
+ // (so the hooks can self-resolve their config on first fire).
24
+ //
25
+ // What it does NOT do:
26
+ // - Overwrite existing hooks (the graphify SessionStart, plugin hooks, etc.
27
+ // are preserved verbatim — we only append).
28
+ // - Set telemetry_url. Default (omitted) → events go to the production API
29
+ // at services.leadconnectorhq.com. Devs can override locally for
30
+ // localhost testing.
31
+ // - Touch ~/.codex/. Codex hook wiring is handled separately (TODO: extend
32
+ // here when we have the same pattern stabilized for Codex).
33
+ //
34
+ // Idempotent: re-running `aw init` is a no-op once everything is in place.
35
+
36
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readdirSync } from 'node:fs';
37
+ import { join, dirname } from 'node:path';
38
+ import { fileURLToPath } from 'node:url';
39
+ import { homedir, hostname, userInfo } from 'node:os';
40
+ import { createHash } from 'node:crypto';
41
+
42
+ const __dirname = dirname(fileURLToPath(import.meta.url));
43
+ // Bundled layout (mirrors aw-ecc/scripts/) so the scripts' relative
44
+ // `require('../lib/aw-usage-telemetry')` paths resolve correctly:
45
+ // libs/aw/hooks/aw-usage/
46
+ // ├── package.json ("type": "commonjs" — overrides parent ESM)
47
+ // ├── hooks/ (7 scripts)
48
+ // └── lib/ (aw-usage-telemetry.js, aw-pricing.js)
49
+ const BUNDLED_ROOT = join(__dirname, 'hooks', 'aw-usage');
50
+ const BUNDLED_HOOKS_DIR = join(BUNDLED_ROOT, 'hooks');
51
+ const BUNDLED_LIB_DIR = join(BUNDLED_ROOT, 'lib');
52
+ const HOME = homedir();
53
+
54
+ // IDE targets we install into. Claude Code is the only fully-wired target.
55
+ // Cursor + Codex are deliberately deferred — copying scripts without wiring
56
+ // the matching hooks.json was misleading (events never fired). A follow-up
57
+ // PR will wire ~/.cursor/hooks/hooks.json properly.
58
+ const IDE_TARGETS = [
59
+ { name: 'Claude Code', configFile: join(HOME, '.claude', 'settings.json'), scriptsRoot: join(HOME, '.claude', 'scripts'), shouldWireConfig: true },
60
+ ];
61
+
62
+ const HOOK_SCRIPTS = [
63
+ 'aw-usage-prompt-submit.js',
64
+ 'aw-usage-post-tool-use.js',
65
+ 'aw-usage-post-tool-use-failure.js',
66
+ 'aw-usage-session-start.js',
67
+ 'aw-usage-stop.js',
68
+ 'aw-usage-telemetry-send.js',
69
+ 'aw-usage-commit-created.js',
70
+ ];
71
+
72
+ const LIB_FILES = [
73
+ 'aw-usage-telemetry.js',
74
+ 'aw-pricing.js',
75
+ ];
76
+
77
+ // Hook description marker — used to detect prior install + idempotent re-apply.
78
+ const MARKER = 'AW usage telemetry';
79
+
80
+ // ── Helpers ──────────────────────────────────────────────────────────
81
+
82
+ function safeMkdir(dir) {
83
+ try { mkdirSync(dir, { recursive: true }); } catch { /* best effort */ }
84
+ }
85
+
86
+ // Tri-state result so callers can distinguish a real failure from a no-op:
87
+ // 'copied' — wrote bytes
88
+ // 'unchanged' — destination already matches source byte-for-byte
89
+ // 'failed' — source missing OR write/read threw (permissions, EBUSY, …)
90
+ function copyIfChanged(src, dest) {
91
+ if (!existsSync(src)) return 'failed';
92
+ try {
93
+ if (existsSync(dest)) {
94
+ const a = readFileSync(src);
95
+ const b = readFileSync(dest);
96
+ if (a.equals(b)) return 'unchanged';
97
+ }
98
+ copyFileSync(src, dest);
99
+ return 'copied';
100
+ } catch { return 'failed'; }
101
+ }
102
+
103
+ function generateMachineId() {
104
+ return createHash('sha256')
105
+ .update(`${hostname()}:${userInfo().username}`)
106
+ .digest('hex');
107
+ }
108
+
109
+ // ── Telemetry config bootstrap ───────────────────────────────────────
110
+
111
+ function ensureTelemetryConfig() {
112
+ const cfgPath = join(HOME, '.aw', 'telemetry', 'config.json');
113
+ safeMkdir(dirname(cfgPath));
114
+ let cfg = {};
115
+ if (existsSync(cfgPath)) {
116
+ try { cfg = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch { cfg = {}; }
117
+ }
118
+ let changed = false;
119
+ if (!cfg.machine_id || cfg.machine_id === 'unknown') {
120
+ cfg.machine_id = generateMachineId();
121
+ changed = true;
122
+ }
123
+ if (cfg.enabled !== false && cfg.enabled !== true) {
124
+ cfg.enabled = true;
125
+ changed = true;
126
+ }
127
+ if (changed) writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
128
+ return { cfgPath, changed };
129
+ }
130
+
131
+ // ── settings.json hook merge ─────────────────────────────────────────
132
+
133
+ function settingsHookEntry(scriptName, description, scriptsRoot, withMatcher) {
134
+ // Use $HOME so the path is portable across machines. Shell expansion
135
+ // applies when Claude invokes the command.
136
+ const portablePath = scriptsRoot
137
+ .replace(HOME, '$HOME')
138
+ .split(/[\\/]/).join('/');
139
+ const cmd = `node "${portablePath}/hooks/${scriptName}"`;
140
+ const entry = {
141
+ hooks: [{
142
+ type: 'command',
143
+ command: cmd,
144
+ async: true,
145
+ timeout: 10,
146
+ }],
147
+ description,
148
+ };
149
+ if (withMatcher) entry.matcher = '*';
150
+ return entry;
151
+ }
152
+
153
+ function alreadyWired(phaseArray, expectedCommand) {
154
+ if (!Array.isArray(phaseArray)) return false;
155
+ return phaseArray.some(group =>
156
+ Array.isArray(group?.hooks) &&
157
+ group.hooks.some(h => h?.command === expectedCommand)
158
+ );
159
+ }
160
+
161
+ function isLegacyAwUsageHookGroup(group, expectedCommand, scriptName) {
162
+ if (!Array.isArray(group?.hooks)) return false;
163
+ const commands = group.hooks
164
+ .map(h => h?.command)
165
+ .filter(command => typeof command === 'string');
166
+ const hasExpected = commands.includes(expectedCommand);
167
+ const hasLegacyAwUsage = commands.some((command) => {
168
+ const normalized = command.split('\\').join('/');
169
+ return command.includes(scriptName) &&
170
+ (normalized.includes('/.aw-ecc/') || command.includes('${CLAUDE_PLUGIN_ROOT}'));
171
+ });
172
+ return !hasExpected && hasLegacyAwUsage;
173
+ }
174
+
175
+ function mergeSettings(configFile, scriptsRoot) {
176
+ safeMkdir(dirname(configFile));
177
+ let cfg = {};
178
+ if (existsSync(configFile)) {
179
+ try { cfg = JSON.parse(readFileSync(configFile, 'utf8')); } catch { cfg = {}; }
180
+ }
181
+ cfg.hooks = cfg.hooks && typeof cfg.hooks === 'object' && !Array.isArray(cfg.hooks)
182
+ ? cfg.hooks
183
+ : {};
184
+
185
+ const phases = [
186
+ { name: 'SessionStart', script: 'aw-usage-session-start.js', desc: `${MARKER}: session_start`, matcher: false },
187
+ { name: 'UserPromptSubmit', script: 'aw-usage-prompt-submit.js', desc: `${MARKER}: prompt_submitted with slash-command detection`, matcher: false },
188
+ { name: 'PostToolUse', script: 'aw-usage-post-tool-use.js', desc: `${MARKER}: skill/agent/error/test/sdlc events`, matcher: true },
189
+ { name: 'PostToolUseFailure', script: 'aw-usage-post-tool-use-failure.js', desc: `${MARKER}: tool_error on failure`, matcher: true },
190
+ { name: 'Stop', script: 'aw-usage-stop.js', desc: `${MARKER}: response_completed with tokens + cost`, matcher: true },
191
+ ];
192
+
193
+ let changed = 0;
194
+ for (const phase of phases) {
195
+ // Normalize: existing config may have a non-array value here from a manual
196
+ // edit or another tool — don't trust truthiness, demand an array.
197
+ cfg.hooks[phase.name] = Array.isArray(cfg.hooks[phase.name]) ? cfg.hooks[phase.name] : [];
198
+ const entry = settingsHookEntry(phase.script, phase.desc, scriptsRoot, phase.matcher);
199
+ const before = cfg.hooks[phase.name].length;
200
+ cfg.hooks[phase.name] = cfg.hooks[phase.name].filter(group =>
201
+ !isLegacyAwUsageHookGroup(group, entry.hooks[0].command, phase.script)
202
+ );
203
+ changed += before - cfg.hooks[phase.name].length;
204
+ if (!alreadyWired(cfg.hooks[phase.name], entry.hooks[0].command)) {
205
+ cfg.hooks[phase.name].push(entry);
206
+ changed++;
207
+ }
208
+ }
209
+
210
+ if (changed > 0) {
211
+ writeFileSync(configFile, JSON.stringify(cfg, null, 2) + '\n');
212
+ }
213
+ return { added: changed, configFile };
214
+ }
215
+
216
+ // ── Main entry ───────────────────────────────────────────────────────
217
+
218
+ /**
219
+ * Install the aw-usage producer hooks into ~/.claude/.
220
+ * Idempotent — safe to re-run.
221
+ *
222
+ * @returns {{
223
+ * scriptsCopied: number,
224
+ * scriptsUnchanged: number,
225
+ * scriptsFailed: number,
226
+ * libCopied: number,
227
+ * libUnchanged: number,
228
+ * libFailed: number,
229
+ * targetsConfigured: string[],
230
+ * telemetryConfigInitialized: boolean,
231
+ * }}
232
+ */
233
+ export function installAwUsageHooks() {
234
+ const result = {
235
+ scriptsCopied: 0,
236
+ scriptsUnchanged: 0,
237
+ scriptsFailed: 0,
238
+ libCopied: 0,
239
+ libUnchanged: 0,
240
+ libFailed: 0,
241
+ targetsConfigured: [],
242
+ telemetryConfigInitialized: false,
243
+ };
244
+
245
+ if (!existsSync(BUNDLED_HOOKS_DIR) || !existsSync(BUNDLED_LIB_DIR)) {
246
+ // Bundled scripts missing — nothing to install. Caller can decide.
247
+ return result;
248
+ }
249
+
250
+ // 1. Copy scripts + lib to each IDE target
251
+ for (const target of IDE_TARGETS) {
252
+ const hooksDest = join(target.scriptsRoot, 'hooks');
253
+ const libDest = join(target.scriptsRoot, 'lib');
254
+ safeMkdir(hooksDest);
255
+ safeMkdir(libDest);
256
+
257
+ for (const name of HOOK_SCRIPTS) {
258
+ const src = join(BUNDLED_HOOKS_DIR, name);
259
+ const dest = join(hooksDest, name);
260
+ const r = copyIfChanged(src, dest);
261
+ if (r === 'copied') result.scriptsCopied++;
262
+ else if (r === 'unchanged') result.scriptsUnchanged++;
263
+ else result.scriptsFailed++;
264
+ }
265
+ for (const name of LIB_FILES) {
266
+ const src = join(BUNDLED_LIB_DIR, name);
267
+ const dest = join(libDest, name);
268
+ const r = copyIfChanged(src, dest);
269
+ if (r === 'copied') result.libCopied++;
270
+ else if (r === 'unchanged') result.libUnchanged++;
271
+ else result.libFailed++;
272
+ }
273
+
274
+ // 2. Merge hook wiring into settings.json (Claude only for now)
275
+ if (target.shouldWireConfig && target.configFile) {
276
+ const { added, configFile } = mergeSettings(target.configFile, target.scriptsRoot);
277
+ if (added > 0) result.targetsConfigured.push(`${target.name}:${configFile}`);
278
+ }
279
+ }
280
+
281
+ // 3. Bootstrap telemetry config
282
+ const { changed } = ensureTelemetryConfig();
283
+ result.telemetryConfigInitialized = changed;
284
+
285
+ return result;
286
+ }
287
+
288
+ /**
289
+ * Print a tidy summary of what installAwUsageHooks did.
290
+ */
291
+ export function formatAwUsageHooksInstallReport(result) {
292
+ const parts = [];
293
+ if (result.scriptsCopied > 0) parts.push(`${result.scriptsCopied} scripts`);
294
+ if (result.libCopied > 0) parts.push(`${result.libCopied} lib files`);
295
+ if (result.targetsConfigured.length > 0) parts.push(`wired ${result.targetsConfigured.length} settings.json`);
296
+ if (result.telemetryConfigInitialized) parts.push('bootstrapped telemetry config');
297
+ // Surface failures even when everything else is a no-op, so an install that
298
+ // silently lost files doesn't masquerade as "already up to date".
299
+ const failedTotal = (result.scriptsFailed || 0) + (result.libFailed || 0);
300
+ if (failedTotal > 0) parts.push(`⚠ ${failedTotal} failed (check ~/.claude/scripts/ permissions)`);
301
+ if (parts.length === 0) return 'aw-usage hooks already up to date';
302
+ return `aw-usage hooks: ${parts.join(' · ')}`;
303
+ }
package/integrations.mjs CHANGED
@@ -24,6 +24,10 @@ const execAsync = promisify(exec);
24
24
  const HOME = homedir();
25
25
  const MANIFEST_PATH = join(HOME, '.aw_registry', '.integration-manifest.json');
26
26
 
27
+ function getErrorMessage(error) {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+
27
31
  // ────────────────────────────────────────────────────────────────────────────────
28
32
  // INTEGRATION REGISTRY
29
33
  // ────────────────────────────────────────────────────────────────────────────────
@@ -109,7 +113,7 @@ export const INTEGRATIONS = {
109
113
  type: 'python-cli',
110
114
  label: 'Graphify (Knowledge Graph)',
111
115
  description: 'Builds a queryable knowledge graph of your codebase + docs',
112
- pipPackage: 'graphifyy',
116
+ pipPackage: 'graphifyy[mcp,pdf,office,svg,sql,google,neo4j]',
113
117
  cliCommand: 'graphify',
114
118
  minPython: { major: 3, minor: 10 },
115
119
  postInstall: [
@@ -204,7 +208,7 @@ function addToJsonMcp(filePath, serverName, config) {
204
208
  try {
205
209
  existing = JSON.parse(readFileSync(filePath, 'utf8'));
206
210
  } catch {
207
- // Corrupted file, start fresh
211
+ existing = {};
208
212
  }
209
213
  }
210
214
 
@@ -244,6 +248,12 @@ function addToTomlMcp(filePath, serverName, url) {
244
248
  const IS_WINDOWS = process.platform === 'win32';
245
249
  const WHICH_CMD = IS_WINDOWS ? 'where' : 'which';
246
250
 
251
+ function quoteHookCommandArg(value) {
252
+ const text = String(value);
253
+ if (IS_WINDOWS) return `"${text.replace(/"/g, '\\"')}"`;
254
+ return "'" + text.replace(/'/g, "'\\''") + "'";
255
+ }
256
+
247
257
  // Try interpreters in order; return the first that meets minPython, or null.
248
258
  async function detectPython({ major, minor }) {
249
259
  const candidates = IS_WINDOWS
@@ -286,14 +296,14 @@ async function pickPythonInstaller(pythonCmd) {
286
296
  // current project's graph into ~/.graphify/global-graph.json every time Claude Code
287
297
  // opens in a project that has a built graph. Fast (JSON merge only, no LLM).
288
298
  // Written globally so one hook covers all projects — guarded by graph.json existence.
289
- export function installGraphifyGlobalAddHook(homeDir) {
299
+ export function installGraphifyGlobalAddHook(homeDir, installerName = 'pip') {
290
300
  const claudeDir = join(homeDir, '.claude');
291
301
  if (!existsSync(claudeDir)) return;
292
302
 
293
303
  const settingsPath = join(claudeDir, 'settings.json');
294
304
  let settings = {};
295
305
  if (existsSync(settingsPath)) {
296
- try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { /* corrupt start fresh */ }
306
+ try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
297
307
  }
298
308
 
299
309
  if (!settings.hooks) settings.hooks = {};
@@ -308,8 +318,35 @@ export function installGraphifyGlobalAddHook(homeDir) {
308
318
  // across future shell sessions where an old graphify binary might shadow the
309
319
  // currently-installed version via PATH. Derived fresh here rather than baking
310
320
  // in the cli token resolved at install time.
311
- const pythonExe = IS_WINDOWS ? 'python' : 'python3';
312
- const cmd = `[ -f graphify-out/graph.json ] && ${pythonExe} -m graphify global add graphify-out/graph.json --as "$(basename "$PWD")" > /dev/null 2>&1 || true`;
321
+ const pythonExe = resolveGraphifyPython(installerName, homeDir);
322
+ const scriptsDir = join(claudeDir, 'scripts');
323
+ const scriptPath = join(scriptsDir, 'graphify-global-add.mjs');
324
+ mkdirSync(scriptsDir, { recursive: true });
325
+ writeFileSync(scriptPath, `import { spawnSync } from 'node:child_process';
326
+ import { existsSync } from 'node:fs';
327
+ import { basename } from 'node:path';
328
+
329
+ const pythonExe = process.argv[2] || (process.platform === 'win32' ? 'python' : 'python3');
330
+ const graphPath = 'graphify-out/graph.json';
331
+
332
+ if (!existsSync(graphPath)) {
333
+ process.exit(0);
334
+ }
335
+
336
+ const result = spawnSync(
337
+ pythonExe,
338
+ ['-m', 'graphify', 'global', 'add', graphPath, '--as', basename(process.cwd())],
339
+ { stdio: 'ignore', windowsHide: true },
340
+ );
341
+
342
+ if (result.error) {
343
+ process.exit(0);
344
+ }
345
+
346
+ process.exit(0);
347
+ `);
348
+
349
+ const cmd = `${quoteHookCommandArg(process.execPath || 'node')} ${quoteHookCommandArg(scriptPath)} ${quoteHookCommandArg(pythonExe)}`;
313
350
 
314
351
  settings.hooks.SessionStart.push({
315
352
  description: MARKER,
@@ -320,38 +357,49 @@ export function installGraphifyGlobalAddHook(homeDir) {
320
357
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
321
358
  }
322
359
 
323
- // Write the graphify-global MCP server entry to ~/.claude/settings.json so Claude Code
324
- // auto-starts it on launch. Uses `python -m graphify.serve` to avoid PATH issues with
325
- // stale graphify.exe binaries that may shadow the currently installed version.
326
- //
327
- // The command is a single stable token — `python` on Windows, `python3` on POSIX —
328
- // so the JSON args array is always parseable (py.cmd can be `py -3` which has a space
329
- // and would break if used directly as the command field).
330
- export function installGraphifyMcpServer(homeDir) {
331
- const claudeDir = join(homeDir, '.claude');
332
- if (!existsSync(claudeDir)) return;
360
+ // Resolve the Python executable that has graphify installed.
361
+ // uv/pipx create isolated venvs system `python` won't find the module.
362
+ function resolveGraphifyPython(installerName, homeDir) {
363
+ if (installerName === 'uv') {
364
+ const appData = IS_WINDOWS
365
+ ? (process.env.APPDATA || join(homeDir, 'AppData', 'Roaming'))
366
+ : join(homeDir, '.local', 'share');
367
+ const candidate = IS_WINDOWS
368
+ ? join(appData, 'uv', 'tools', 'graphifyy', 'Scripts', 'python.exe')
369
+ : join(appData, 'uv', 'tools', 'graphifyy', 'bin', 'python');
370
+ if (existsSync(candidate)) return candidate;
371
+ }
372
+ if (installerName === 'pipx') {
373
+ const candidate = IS_WINDOWS
374
+ ? join(homeDir, '.local', 'pipx', 'venvs', 'graphifyy', 'Scripts', 'python.exe')
375
+ : join(homeDir, '.local', 'pipx', 'venvs', 'graphifyy', 'bin', 'python');
376
+ if (existsSync(candidate)) return candidate;
377
+ }
378
+ // pip --user: graphify lands in system site-packages, system python works.
379
+ return IS_WINDOWS ? 'python' : 'python3';
380
+ }
333
381
 
334
- const settingsPath = join(claudeDir, 'settings.json');
335
- let settings = {};
336
- if (existsSync(settingsPath)) {
337
- try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { /* corrupt — start fresh */ }
382
+ // Write the graphify-global MCP server entry to ~/.claude.json so Claude Code CLI
383
+ // picks it up via `claude mcp list`. Uses the venv Python for uv/pipx installs so
384
+ // `python -m graphify.serve` resolves correctly (system python won't find the module).
385
+ export function installGraphifyMcpServer(homeDir, installerName = 'pip') {
386
+ const claudeJsonPath = join(homeDir, '.claude.json');
387
+ let config = {};
388
+ if (existsSync(claudeJsonPath)) {
389
+ try { config = JSON.parse(readFileSync(claudeJsonPath, 'utf8')); } catch { config = {}; }
338
390
  }
339
391
 
340
- if (!settings.mcpServers) settings.mcpServers = {};
392
+ if (!config.mcpServers) config.mcpServers = {};
341
393
 
342
- // Use join() so the path uses OS-correct separators on all platforms.
343
394
  const globalGraphPath = join(homeDir, '.graphify', 'global-graph.json');
344
- // `python` on Windows (Microsoft Store + standard installer both put it on PATH),
345
- // `python3` on macOS/Linux (standard convention; `python` may not exist).
346
- const pythonExe = IS_WINDOWS ? 'python' : 'python3';
395
+ const pythonExe = resolveGraphifyPython(installerName, homeDir);
347
396
 
348
- settings.mcpServers['graphify-global'] = {
397
+ config.mcpServers['graphify-global'] = {
349
398
  command: pythonExe,
350
399
  args: ['-m', 'graphify.serve', globalGraphPath],
351
400
  };
352
401
 
353
- mkdirSync(claudeDir, { recursive: true });
354
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
402
+ writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2) + '\n');
355
403
  }
356
404
 
357
405
  // After pip install, the CLI binary may not be on PATH (pip --user puts it in a Scripts
@@ -436,8 +484,8 @@ async function runPythonCli(integration, key, { silent = false } = {}) {
436
484
  // The graph itself is built manually via `/graphify .` — graphify's own per-IDE
437
485
  // install (step 6 below) writes the CLAUDE.md / AGENTS.md sections for that.
438
486
  if (integration.cliCommand === 'graphify') {
439
- installGraphifyMcpServer(HOME);
440
- installGraphifyGlobalAddHook(HOME);
487
+ installGraphifyMcpServer(HOME, installer.name);
488
+ installGraphifyGlobalAddHook(HOME, installer.name);
441
489
  }
442
490
 
443
491
  // 6. Run per-project hooks if cwd looks like a real project (not HOME).
@@ -540,7 +588,9 @@ async function runUniversalInstaller(integration, key, { silent = false } = {})
540
588
  // Verify bash is actually available inside WSL
541
589
  execSync('wsl -- bash --version', { stdio: 'ignore' });
542
590
  wslOk = true;
543
- } catch { /* WSL absent or bash not installed */ }
591
+ } catch {
592
+ wslOk = false;
593
+ }
544
594
 
545
595
  if (wslOk) {
546
596
  cmd = `wsl -- bash -c "curl -fsSL '${integration.scripts.posix}' | sh"`;
@@ -730,8 +780,8 @@ export async function removeIntegration(key, { silent = false } = {}) {
730
780
  delete config.mcpServers?.[key];
731
781
  writeFileSync(claudePath, JSON.stringify(config, null, 2) + '\n');
732
782
  }
733
- } catch {
734
- // Best effort
783
+ } catch (error) {
784
+ if (!silent) fmt.logWarn(`Claude MCP cleanup skipped: ${getErrorMessage(error)}`, 'Cleanup Warning');
735
785
  }
736
786
 
737
787
  // Cursor
@@ -742,8 +792,8 @@ export async function removeIntegration(key, { silent = false } = {}) {
742
792
  delete config.mcpServers?.[key];
743
793
  writeFileSync(cursorPath, JSON.stringify(config, null, 2) + '\n');
744
794
  }
745
- } catch {
746
- // Best effort
795
+ } catch (error) {
796
+ if (!silent) fmt.logWarn(`Cursor MCP cleanup skipped: ${getErrorMessage(error)}`, 'Cleanup Warning');
747
797
  }
748
798
 
749
799
  // Codex
@@ -758,8 +808,8 @@ export async function removeIntegration(key, { silent = false } = {}) {
758
808
  content = content.replace(blockRegex, '');
759
809
  writeFileSync(codexPath, content);
760
810
  }
761
- } catch {
762
- // Best effort
811
+ } catch (error) {
812
+ if (!silent) fmt.logWarn(`Codex MCP cleanup skipped: ${getErrorMessage(error)}`, 'Cleanup Warning');
763
813
  }
764
814
 
765
815
  if (!silent) fmt.logSuccess(`${integration.label} removed from MCP servers`);
@@ -770,7 +820,7 @@ export async function removeIntegration(key, { silent = false } = {}) {
770
820
  fmt.logWarn(
771
821
  `Removed ${integration.label} from the aw manifest. The Python package was left installed.\n` +
772
822
  ` To fully remove, run in each project: \`${integration.cliCommand} claude uninstall\` and \`${integration.cliCommand} hook uninstall\`\n` +
773
- ` Then uninstall the package: \`pip uninstall ${integration.pipPackage.split(/[<>=]/)[0]}\` (or \`uv tool uninstall\` / \`pipx uninstall\`)`,
823
+ ` Then uninstall the package: \`pip uninstall ${integration.pipPackage.split(/[<>=\[]/)[0]}\` (or \`uv tool uninstall\` / \`pipx uninstall\`)`,
774
824
  'Manual Cleanup',
775
825
  );
776
826
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.55",
3
+ "version": "0.1.56-beta.0",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,7 @@
33
33
  "update.mjs",
34
34
  "hooks.mjs",
35
35
  "hooks/",
36
+ "install-aw-usage-hooks.mjs",
36
37
  "startup.mjs",
37
38
  "ecc.mjs",
38
39
  "integrations.mjs",
@@ -40,7 +41,7 @@
40
41
  "telemetry.mjs"
41
42
  ],
42
43
  "engines": {
43
- "node": ">=18.0.0"
44
+ "node": ">=20.0.0"
44
45
  },
45
46
  "keywords": [
46
47
  "ai",