@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.
- package/commands/init.mjs +24 -4
- package/ecc.mjs +1 -1
- package/hooks/aw-usage/hooks/aw-usage-commit-created.js +32 -0
- package/hooks/aw-usage/hooks/aw-usage-post-tool-use-failure.js +67 -0
- package/hooks/aw-usage/hooks/aw-usage-post-tool-use.js +373 -0
- package/hooks/aw-usage/hooks/aw-usage-prompt-submit.js +173 -0
- package/hooks/aw-usage/hooks/aw-usage-session-start.js +48 -0
- package/hooks/aw-usage/hooks/aw-usage-stop.js +182 -0
- package/hooks/aw-usage/hooks/aw-usage-telemetry-send.js +84 -0
- package/hooks/aw-usage/lib/aw-pricing.js +306 -0
- package/hooks/aw-usage/lib/aw-usage-telemetry.js +503 -0
- package/hooks/aw-usage/package.json +4 -0
- package/install-aw-usage-hooks.mjs +303 -0
- package/integrations.mjs +88 -38
- package/package.json +3 -2
|
@@ -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
|
-
|
|
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 {
|
|
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 =
|
|
312
|
-
const
|
|
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
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
397
|
+
config.mcpServers['graphify-global'] = {
|
|
349
398
|
command: pythonExe,
|
|
350
399
|
args: ['-m', 'graphify.serve', globalGraphPath],
|
|
351
400
|
};
|
|
352
401
|
|
|
353
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(/[
|
|
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.
|
|
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": ">=
|
|
44
|
+
"node": ">=20.0.0"
|
|
44
45
|
},
|
|
45
46
|
"keywords": [
|
|
46
47
|
"ai",
|