@ghl-ai/aw 0.1.92 → 0.2.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/aw-usage-wiring.mjs +386 -0
- package/codex.mjs +74 -19
- package/commands/c4.mjs +7 -5
- package/commands/init.mjs +89 -28
- package/ecc.mjs +1 -1
- package/hook-cleanup.mjs +4 -3
- package/hooks/aw-usage/hooks/aw-usage-commit-created.js +21 -0
- package/hooks/aw-usage/hooks/aw-usage-post-tool-use.js +159 -33
- package/hooks/aw-usage/hooks/aw-usage-prompt-submit.js +39 -74
- package/hooks/aw-usage/hooks/aw-usage-telemetry-send.js +67 -7
- package/hooks/aw-usage/lib/aw-usage-commands.js +122 -0
- package/hooks/aw-usage/lib/aw-usage-envelope.js +121 -0
- package/hooks/aw-usage/lib/aw-usage-queue.js +168 -0
- package/hooks/aw-usage/lib/aw-usage-skill-source.js +138 -0
- package/hooks/aw-usage/lib/aw-usage-stage-events.js +179 -0
- package/hooks/aw-usage/lib/aw-usage-telemetry.js +163 -3
- package/hooks/aw-usage/package.json +1 -1
- package/hooks/codex-home.mjs +8 -6
- package/hooks.mjs +5 -8
- package/install-aw-usage-hooks.mjs +65 -215
- package/package.json +2 -1
- package/startup.mjs +54 -155
|
@@ -1,105 +1,41 @@
|
|
|
1
|
-
// install-aw-usage-hooks.mjs —
|
|
1
|
+
// install-aw-usage-hooks.mjs — install the aw-usage telemetry hooks on `aw init`.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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.
|
|
3
|
+
// This repo is the single source of truth for the aw-usage hook scripts
|
|
4
|
+
// (libs/aw/hooks/aw-usage/). aw-ecc no longer ships or wires them.
|
|
12
5
|
//
|
|
13
6
|
// What it does on `aw init`:
|
|
14
|
-
// 1. Copies bundled
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
7
|
+
// 1. Copies the bundled scripts to ~/.aw/aw-usage/{hooks,lib,package.json}
|
|
8
|
+
// — one harness-neutral location used by Claude Code, Codex, git
|
|
9
|
+
// post-commit and the Cursor adapter.
|
|
10
|
+
// 2. Merges the Claude Code entries into ~/.claude/settings.json: one
|
|
11
|
+
// "AW usage telemetry" entry per phase (SessionStart, UserPromptSubmit,
|
|
12
|
+
// PostToolUse, PostToolUseFailure, Stop) plus one
|
|
13
|
+
// "AW-managed: aw-memory stop hooks" entry. Legacy entries pointing at
|
|
14
|
+
// ~/.aw-ecc, ${CLAUDE_PLUGIN_ROOT} or ~/.claude/scripts are replaced;
|
|
15
|
+
// unrelated hooks are kept in order.
|
|
16
|
+
// 3. Ensures ~/.aw/telemetry/config.json exists with machine_id + enabled.
|
|
24
17
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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).
|
|
18
|
+
// startup.mjs (aw link / pull / startup / post-upgrade) runs the same copy
|
|
19
|
+
// and merge through aw-usage-wiring.mjs, so both writers always agree.
|
|
33
20
|
//
|
|
34
|
-
//
|
|
21
|
+
// It does NOT set telemetry_url (default: production API) and does not touch
|
|
22
|
+
// ~/.codex (see hooks/codex-home.mjs). Idempotent: re-running is a no-op.
|
|
35
23
|
|
|
36
|
-
import { readFileSync, writeFileSync, mkdirSync,
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
37
25
|
import { join, dirname } from 'node:path';
|
|
38
|
-
import { fileURLToPath } from 'node:url';
|
|
39
26
|
import { homedir, hostname, userInfo } from 'node:os';
|
|
40
27
|
import { createHash } from 'node:crypto';
|
|
28
|
+
import {
|
|
29
|
+
copyAwUsageScripts,
|
|
30
|
+
mergeClaudeAwUsageHooks,
|
|
31
|
+
missingAwUsageFiles,
|
|
32
|
+
} from './aw-usage-wiring.mjs';
|
|
41
33
|
|
|
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
34
|
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';
|
|
35
|
+
const CLAUDE_SETTINGS = join(HOME, '.claude', 'settings.json');
|
|
79
36
|
|
|
80
37
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
81
38
|
|
|
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
39
|
function generateMachineId() {
|
|
104
40
|
return createHash('sha256')
|
|
105
41
|
.update(`${hostname()}:${userInfo().username}`)
|
|
@@ -110,7 +46,7 @@ function generateMachineId() {
|
|
|
110
46
|
|
|
111
47
|
function ensureTelemetryConfig() {
|
|
112
48
|
const cfgPath = join(HOME, '.aw', 'telemetry', 'config.json');
|
|
113
|
-
|
|
49
|
+
mkdirSync(dirname(cfgPath), { recursive: true });
|
|
114
50
|
let cfg = {};
|
|
115
51
|
if (existsSync(cfgPath)) {
|
|
116
52
|
try { cfg = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch { cfg = {}; }
|
|
@@ -130,93 +66,33 @@ function ensureTelemetryConfig() {
|
|
|
130
66
|
|
|
131
67
|
// ── settings.json hook merge ─────────────────────────────────────────
|
|
132
68
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
}
|
|
69
|
+
// Returns null when the file exists but is not a JSON object, so callers
|
|
70
|
+
// never overwrite (and wipe) a settings.json they cannot understand.
|
|
71
|
+
function readSettings(configFile) {
|
|
72
|
+
if (!existsSync(configFile)) return {};
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(readFileSync(configFile, 'utf8'));
|
|
75
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
208
78
|
}
|
|
79
|
+
}
|
|
209
80
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
81
|
+
/** @returns {'written'|'unchanged'|'unreadable'} */
|
|
82
|
+
function mergeSettings(configFile, installed) {
|
|
83
|
+
const cfg = readSettings(configFile);
|
|
84
|
+
if (cfg === null) return 'unreadable';
|
|
85
|
+
const hooks = mergeClaudeAwUsageHooks(cfg.hooks, { installed });
|
|
86
|
+
if (JSON.stringify(hooks) === JSON.stringify(cfg.hooks)) return 'unchanged';
|
|
87
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
88
|
+
writeFileSync(configFile, JSON.stringify({ ...cfg, hooks }, null, 2) + '\n');
|
|
89
|
+
return 'written';
|
|
214
90
|
}
|
|
215
91
|
|
|
216
92
|
// ── Main entry ───────────────────────────────────────────────────────
|
|
217
93
|
|
|
218
94
|
/**
|
|
219
|
-
* Install the aw-usage
|
|
95
|
+
* Install the aw-usage hooks into ~/.aw/aw-usage and ~/.claude/settings.json.
|
|
220
96
|
* Idempotent — safe to re-run.
|
|
221
97
|
*
|
|
222
98
|
* @returns {{
|
|
@@ -227,61 +103,34 @@ function mergeSettings(configFile, scriptsRoot) {
|
|
|
227
103
|
* libUnchanged: number,
|
|
228
104
|
* libFailed: number,
|
|
229
105
|
* targetsConfigured: string[],
|
|
106
|
+
* settingsUnreadable: string | null,
|
|
230
107
|
* telemetryConfigInitialized: boolean,
|
|
231
108
|
* }}
|
|
232
109
|
*/
|
|
233
110
|
export function installAwUsageHooks() {
|
|
111
|
+
const copy = copyAwUsageScripts(HOME);
|
|
234
112
|
const result = {
|
|
235
|
-
scriptsCopied:
|
|
236
|
-
scriptsUnchanged:
|
|
237
|
-
scriptsFailed:
|
|
238
|
-
libCopied:
|
|
239
|
-
libUnchanged:
|
|
240
|
-
|
|
113
|
+
scriptsCopied: copy.byDir.hooks.copied,
|
|
114
|
+
scriptsUnchanged: copy.byDir.hooks.unchanged,
|
|
115
|
+
scriptsFailed: copy.byDir.hooks.failed,
|
|
116
|
+
libCopied: copy.byDir.lib.copied,
|
|
117
|
+
libUnchanged: copy.byDir.lib.unchanged,
|
|
118
|
+
// package.json is part of the lib layer: it makes require() work.
|
|
119
|
+
libFailed: copy.byDir.lib.failed + copy.byDir.root.failed,
|
|
241
120
|
targetsConfigured: [],
|
|
121
|
+
settingsUnreadable: null,
|
|
242
122
|
telemetryConfigInitialized: false,
|
|
243
123
|
};
|
|
244
124
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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;
|
|
125
|
+
// If any file failed to copy, mergeSettings keeps the existing entries
|
|
126
|
+
// instead of pointing Claude at an incomplete ~/.aw/aw-usage. A file can
|
|
127
|
+
// exist and still fail to copy (unreadable, read-only), so check both.
|
|
128
|
+
const installed = copy.failed === 0 && missingAwUsageFiles(HOME).length === 0;
|
|
129
|
+
const merge = mergeSettings(CLAUDE_SETTINGS, installed);
|
|
130
|
+
if (merge === 'written') result.targetsConfigured.push(`Claude Code:${CLAUDE_SETTINGS}`);
|
|
131
|
+
if (merge === 'unreadable') result.settingsUnreadable = CLAUDE_SETTINGS;
|
|
284
132
|
|
|
133
|
+
result.telemetryConfigInitialized = ensureTelemetryConfig().changed;
|
|
285
134
|
return result;
|
|
286
135
|
}
|
|
287
136
|
|
|
@@ -297,7 +146,8 @@ export function formatAwUsageHooksInstallReport(result) {
|
|
|
297
146
|
// Surface failures even when everything else is a no-op, so an install that
|
|
298
147
|
// silently lost files doesn't masquerade as "already up to date".
|
|
299
148
|
const failedTotal = (result.scriptsFailed || 0) + (result.libFailed || 0);
|
|
300
|
-
if (failedTotal > 0) parts.push(`⚠ ${failedTotal} failed (check ~/.
|
|
149
|
+
if (failedTotal > 0) parts.push(`⚠ ${failedTotal} failed (check ~/.aw/aw-usage/ permissions)`);
|
|
150
|
+
if (result.settingsUnreadable) parts.push(`⚠ ${result.settingsUnreadable} is not valid JSON, left unchanged`);
|
|
301
151
|
if (parts.length === 0) return 'aw-usage hooks already up to date';
|
|
302
152
|
return `aw-usage hooks: ${parts.join(' · ')}`;
|
|
303
153
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"update.mjs",
|
|
37
37
|
"hooks.mjs",
|
|
38
38
|
"hooks/",
|
|
39
|
+
"aw-usage-wiring.mjs",
|
|
39
40
|
"install-aw-usage-hooks.mjs",
|
|
40
41
|
"startup.mjs",
|
|
41
42
|
"ecc.mjs",
|
package/startup.mjs
CHANGED
|
@@ -9,6 +9,17 @@ import {
|
|
|
9
9
|
getCodexHomePhaseDefinitions,
|
|
10
10
|
isManagedCodexHomeEntry,
|
|
11
11
|
} from './hooks/codex-home.mjs';
|
|
12
|
+
import {
|
|
13
|
+
copyAwUsageScripts,
|
|
14
|
+
ensureAwUsageVersion,
|
|
15
|
+
mergeClaudeAwUsageHooks,
|
|
16
|
+
missingAwUsageFiles,
|
|
17
|
+
removeClaudeAwUsageHooks,
|
|
18
|
+
warnIncompleteAwUsageInstall,
|
|
19
|
+
warnStuckAwUsageFiles,
|
|
20
|
+
warnUnreadableClaudeSettings,
|
|
21
|
+
} from './aw-usage-wiring.mjs';
|
|
22
|
+
import { codexHooksFeatureEnabled, ensureCodexHooksFeature } from './codex.mjs';
|
|
12
23
|
|
|
13
24
|
const STARTUP_PREFS_FILENAME = 'startup-preferences.json';
|
|
14
25
|
const ENABLED_MODE = 'enabled';
|
|
@@ -33,34 +44,6 @@ const CLAUDE_ROUTER_COMMAND =
|
|
|
33
44
|
'for f in "$HOME/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh" "$HOME/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"; do [ -f "$f" ] && exec bash "$f"; done; exit 0';
|
|
34
45
|
const CLAUDE_REMINDER_COMMAND =
|
|
35
46
|
'bash -lc \'exec bash "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/hooks/session-start-rules-context.sh"\'';
|
|
36
|
-
const CLAUDE_TELEMETRY_DESCRIPTION = 'AW usage telemetry';
|
|
37
|
-
const CLAUDE_TELEMETRY_HOOKS = [
|
|
38
|
-
{
|
|
39
|
-
phase: 'SessionStart',
|
|
40
|
-
matcher: undefined,
|
|
41
|
-
command: 'test -f "$HOME/.aw-ecc/scripts/hooks/aw-usage-session-start.js" && node "$HOME/.aw-ecc/scripts/hooks/aw-usage-session-start.js" || true',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
phase: 'PostToolUse',
|
|
45
|
-
matcher: 'Skill|Agent|Shell|Bash',
|
|
46
|
-
command: 'test -f "$HOME/.aw-ecc/scripts/hooks/aw-usage-post-tool-use.js" && node "$HOME/.aw-ecc/scripts/hooks/aw-usage-post-tool-use.js" || true',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
phase: 'Stop',
|
|
50
|
-
matcher: undefined,
|
|
51
|
-
command: 'STDIN="$(cat)"; test -f "$HOME/.aw-ecc/scripts/hooks/aw-usage-stop.js" && printf \'%s\' "$STDIN" | node "$HOME/.aw-ecc/scripts/hooks/aw-usage-stop.js" || true; test -f "$HOME/.aw-ecc/scripts/hooks/aw-memory-intent-capture.js" && printf \'%s\' "$STDIN" | node "$HOME/.aw-ecc/scripts/hooks/aw-memory-intent-capture.js" || true; test -f "$HOME/.aw-ecc/scripts/hooks/aw-memory-sync.js" && printf \'%s\' "$STDIN" | node "$HOME/.aw-ecc/scripts/hooks/aw-memory-sync.js" || true',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
phase: 'PostToolUseFailure',
|
|
55
|
-
matcher: undefined,
|
|
56
|
-
command: 'test -f "$HOME/.aw-ecc/scripts/hooks/aw-usage-post-tool-use-failure.js" && node "$HOME/.aw-ecc/scripts/hooks/aw-usage-post-tool-use-failure.js" || true',
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
phase: 'UserPromptSubmit',
|
|
60
|
-
matcher: undefined,
|
|
61
|
-
command: 'test -f "$HOME/.aw-ecc/scripts/hooks/aw-usage-prompt-submit.js" && node "$HOME/.aw-ecc/scripts/hooks/aw-usage-prompt-submit.js" || true',
|
|
62
|
-
},
|
|
63
|
-
];
|
|
64
47
|
const CURSOR_SESSION_START_COMMAND = 'node .cursor/hooks/session-start.js';
|
|
65
48
|
const CURSOR_SESSION_START_DESCRIPTION = 'Load previous context and detect environment';
|
|
66
49
|
const REPO_CURSOR_SESSION_START_COMMAND = 'bash "$(git rev-parse --show-toplevel)/hooks/aw-session-start"';
|
|
@@ -257,86 +240,42 @@ function hasCommandHook(entry, command) {
|
|
|
257
240
|
);
|
|
258
241
|
}
|
|
259
242
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
function buildClaudeTelemetryEntry(hookDef) {
|
|
268
|
-
const entry = {
|
|
269
|
-
hooks: [
|
|
270
|
-
{
|
|
271
|
-
type: 'command',
|
|
272
|
-
command: hookDef.command,
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
description: CLAUDE_TELEMETRY_DESCRIPTION,
|
|
276
|
-
};
|
|
277
|
-
if (hookDef.matcher) {
|
|
278
|
-
entry.matcher = hookDef.matcher;
|
|
279
|
-
}
|
|
280
|
-
return entry;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Resolve the filesystem path from a telemetry hook command.
|
|
285
|
-
* Commands look like: node "$HOME/.aw-ecc/scripts/hooks/aw-usage-stop.js"
|
|
286
|
-
*/
|
|
287
|
-
function resolveHookScriptPath(command, homeDir) {
|
|
288
|
-
const match = command.match(/"\$HOME\/([^"]+)"/);
|
|
289
|
-
if (match) return join(homeDir, match[1]);
|
|
290
|
-
const matchBare = command.match(/\$HOME\/(\S+)/);
|
|
291
|
-
if (matchBare) return join(homeDir, matchBare[1]);
|
|
243
|
+
// null = settings.json exists but is not a JSON object. Every Claude writer
|
|
244
|
+
// here returns early on null so an unreadable file is never replaced.
|
|
245
|
+
function readClaudeSettings(settingsPath) {
|
|
246
|
+
if (!existsSync(settingsPath)) return {};
|
|
247
|
+
const config = readJson(settingsPath, null);
|
|
248
|
+
if (isObject(config)) return config;
|
|
249
|
+
warnUnreadableClaudeSettings(settingsPath);
|
|
292
250
|
return null;
|
|
293
251
|
}
|
|
294
252
|
|
|
253
|
+
// Claude aw-usage telemetry + AW memory Stop entries. The shape and the merge
|
|
254
|
+
// live in aw-usage-wiring.mjs so this writer and install-aw-usage-hooks.mjs
|
|
255
|
+
// always produce the same settings.json.
|
|
295
256
|
function enableClaudeTelemetryHooks(homeDir = homedir()) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// On version downgrade the aw-ecc clone may not have them — registering
|
|
308
|
-
// hooks for missing scripts causes MODULE_NOT_FOUND crashes.
|
|
309
|
-
const scriptPath = resolveHookScriptPath(hookDef.command, homeDir);
|
|
310
|
-
if (scriptPath && !existsSync(scriptPath)) {
|
|
311
|
-
// Script missing — remove any stale telemetry entries for this phase
|
|
312
|
-
if (managed.length > 0) {
|
|
313
|
-
const cleaned = current.filter(e => !isManagedClaudeTelemetryEntry(e));
|
|
314
|
-
if (cleaned.length > 0) {
|
|
315
|
-
config.hooks[hookDef.phase] = cleaned;
|
|
316
|
-
} else {
|
|
317
|
-
delete config.hooks[hookDef.phase];
|
|
318
|
-
}
|
|
319
|
-
changed = true;
|
|
320
|
-
}
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
257
|
+
// Refresh the scripts first: Codex wrappers and git hooks run them too,
|
|
258
|
+
// and this path runs after every aw link / pull / upgrade.
|
|
259
|
+
const copy = copyAwUsageScripts(homeDir);
|
|
260
|
+
const absent = missingAwUsageFiles(homeDir);
|
|
261
|
+
// On disk but not refreshed (read-only, unreadable): the old content stays.
|
|
262
|
+
const stuck = copy.missing.filter(rel => !absent.includes(rel));
|
|
263
|
+
if (absent.length > 0) warnIncompleteAwUsageInstall(absent);
|
|
264
|
+
// The version warning already names a stuck package.json, with both versions.
|
|
265
|
+
const versionWarned = absent.length === 0 && !ensureAwUsageVersion(homeDir);
|
|
266
|
+
const unreported = versionWarned ? stuck.filter(rel => rel !== 'package.json') : stuck;
|
|
267
|
+
if (unreported.length > 0) warnStuckAwUsageFiles(unreported);
|
|
323
268
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// Remove all legacy/duplicate telemetry entries, then add the canonical one
|
|
328
|
-
const cleaned = current.filter(e => !isManagedClaudeTelemetryEntry(e));
|
|
329
|
-
config.hooks[hookDef.phase] = [...cleaned, buildClaudeTelemetryEntry(hookDef)];
|
|
330
|
-
changed = true;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (!changed) return [];
|
|
269
|
+
const settingsPath = join(homeDir, '.claude', 'settings.json');
|
|
270
|
+
const config = readClaudeSettings(settingsPath);
|
|
271
|
+
if (config === null) return [];
|
|
334
272
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
273
|
+
// Old and new scripts do not mix, so a stuck file blocks wiring like an absent one.
|
|
274
|
+
const installed = absent.length === 0 && stuck.length === 0;
|
|
275
|
+
const hooks = mergeClaudeAwUsageHooks(config.hooks, { installed });
|
|
276
|
+
if (JSON.stringify(hooks) === JSON.stringify(config.hooks)) return [];
|
|
338
277
|
|
|
339
|
-
writeJson(settingsPath, config);
|
|
278
|
+
writeJson(settingsPath, { ...config, hooks });
|
|
340
279
|
return [settingsPath];
|
|
341
280
|
}
|
|
342
281
|
|
|
@@ -344,36 +283,17 @@ function disableClaudeTelemetryHooks(homeDir = homedir()) {
|
|
|
344
283
|
const settingsPath = join(homeDir, '.claude', 'settings.json');
|
|
345
284
|
if (!existsSync(settingsPath)) return [];
|
|
346
285
|
|
|
347
|
-
const config =
|
|
348
|
-
if (!isObject(config.hooks)) return [];
|
|
349
|
-
|
|
350
|
-
let changed = false;
|
|
351
|
-
|
|
352
|
-
for (const hookDef of CLAUDE_TELEMETRY_HOOKS) {
|
|
353
|
-
const current = Array.isArray(config.hooks[hookDef.phase]) ? config.hooks[hookDef.phase] : null;
|
|
354
|
-
if (!current) continue;
|
|
286
|
+
const config = readClaudeSettings(settingsPath);
|
|
287
|
+
if (config === null || !isObject(config.hooks)) return [];
|
|
355
288
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
changed = true;
|
|
359
|
-
if (filtered.length > 0) {
|
|
360
|
-
config.hooks[hookDef.phase] = filtered;
|
|
361
|
-
} else {
|
|
362
|
-
delete config.hooks[hookDef.phase];
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (!changed) return [];
|
|
289
|
+
const hooks = removeClaudeAwUsageHooks(config.hooks);
|
|
290
|
+
if (JSON.stringify(hooks) === JSON.stringify(config.hooks)) return [];
|
|
368
291
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (!pruneEmptySettingsFile(settingsPath, config)) {
|
|
374
|
-
writeJson(settingsPath, config);
|
|
292
|
+
const next = { ...config, hooks };
|
|
293
|
+
if (isEmptyObject(hooks)) delete next.hooks;
|
|
294
|
+
if (!pruneEmptySettingsFile(settingsPath, next)) {
|
|
295
|
+
writeJson(settingsPath, next);
|
|
375
296
|
}
|
|
376
|
-
|
|
377
297
|
return [settingsPath];
|
|
378
298
|
}
|
|
379
299
|
|
|
@@ -410,7 +330,8 @@ function isManagedCodexStopEntry(entry) {
|
|
|
410
330
|
|
|
411
331
|
function disableClaudeStartup(homeDir = homedir()) {
|
|
412
332
|
const settingsPath = join(homeDir, '.claude', 'settings.json');
|
|
413
|
-
const config =
|
|
333
|
+
const config = readClaudeSettings(settingsPath);
|
|
334
|
+
if (config === null) return [];
|
|
414
335
|
ensureHooksObject(config);
|
|
415
336
|
|
|
416
337
|
const before = JSON.stringify(config.hooks);
|
|
@@ -481,7 +402,8 @@ export function purgeManagedClaudeStartupHooks(homeDir = homedir()) {
|
|
|
481
402
|
|
|
482
403
|
function enableClaudeStartup(homeDir = homedir()) {
|
|
483
404
|
const settingsPath = join(homeDir, '.claude', 'settings.json');
|
|
484
|
-
const config =
|
|
405
|
+
const config = readClaudeSettings(settingsPath);
|
|
406
|
+
if (config === null) return [];
|
|
485
407
|
ensureHooksObject(config);
|
|
486
408
|
|
|
487
409
|
const before = JSON.stringify(config.hooks);
|
|
@@ -551,40 +473,17 @@ function hasCodexHooksEnabled(homeDir = homedir()) {
|
|
|
551
473
|
if (!existsSync(configPath)) return false;
|
|
552
474
|
|
|
553
475
|
try {
|
|
554
|
-
return
|
|
476
|
+
return codexHooksFeatureEnabled(readFileSync(configPath, 'utf8'));
|
|
555
477
|
} catch {
|
|
556
478
|
return false;
|
|
557
479
|
}
|
|
558
480
|
}
|
|
559
481
|
|
|
560
|
-
function ensureCodexHookEnabled(content) {
|
|
561
|
-
if (/^\s*codex_hooks\s*=\s*true\b/m.test(content)) {
|
|
562
|
-
return content;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (/^\s*codex_hooks\s*=\s*false\b/m.test(content)) {
|
|
566
|
-
return content.replace(/^\s*codex_hooks\s*=\s*false\b.*$/m, 'codex_hooks = true');
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const featuresHeaderMatch = content.match(/^\[features\]\s*$/m);
|
|
570
|
-
if (featuresHeaderMatch && featuresHeaderMatch.index !== undefined) {
|
|
571
|
-
const insertAt = featuresHeaderMatch.index + featuresHeaderMatch[0].length;
|
|
572
|
-
return `${content.slice(0, insertAt)}\ncodex_hooks = true${content.slice(insertAt)}`;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const trimmed = content.trimEnd();
|
|
576
|
-
if (trimmed.length === 0) {
|
|
577
|
-
return '[features]\ncodex_hooks = true\n';
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return `${trimmed}\n\n[features]\ncodex_hooks = true\n`;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
482
|
function enableCodexStartup(homeDir = homedir()) {
|
|
584
483
|
const updatedFiles = [];
|
|
585
484
|
const configPath = codexConfigPath(homeDir);
|
|
586
485
|
const currentConfig = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
|
|
587
|
-
const nextConfig =
|
|
486
|
+
const nextConfig = ensureCodexHooksFeature(currentConfig);
|
|
588
487
|
|
|
589
488
|
if (!existsSync(configPath) || nextConfig !== currentConfig) {
|
|
590
489
|
mkdirSync(dirname(configPath), { recursive: true });
|