@ghl-ai/aw 0.1.73-beta.1 → 0.1.73-beta.3
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
CHANGED
|
@@ -77,6 +77,19 @@ function writeHookManifestBestEffort(manifest, context) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function installAwUsageHooksBestEffort({ silent = false } = {}) {
|
|
81
|
+
ensureTelemetryConfig();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
if (!isDefaultRoutingEnabled(HOME, process.env)) return null;
|
|
85
|
+
const result = installAwUsageHooks();
|
|
86
|
+
return formatAwUsageHooksInstallReport(result);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (!silent) fmt.note(`aw-usage hooks install: ${e.message}`, 'Telemetry');
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
function formatIntegrationStatusSummary(statuses, installedNow = []) {
|
|
81
94
|
if (!statuses || statuses.length === 0) return null;
|
|
82
95
|
|
|
@@ -506,6 +519,8 @@ export async function initCommand(args) {
|
|
|
506
519
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
507
520
|
if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
508
521
|
|
|
522
|
+
const awUsageHooksReport = installAwUsageHooksBestEffort({ silent });
|
|
523
|
+
|
|
509
524
|
// Write hook manifest after all hook installation is complete
|
|
510
525
|
writeHookManifestBestEffort({ eccVersion: AW_ECC_TAG, awVersion: VERSION });
|
|
511
526
|
|
|
@@ -533,6 +548,7 @@ export async function initCommand(args) {
|
|
|
533
548
|
'',
|
|
534
549
|
` ${chalk.green('✓')} Registry synced`,
|
|
535
550
|
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
551
|
+
awUsageHooksReport ? ` ${chalk.green('✓')} ${awUsageHooksReport}` : null,
|
|
536
552
|
removedLegacyStartupFiles.length > 0
|
|
537
553
|
? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
|
|
538
554
|
: null,
|
|
@@ -680,25 +696,12 @@ export async function initCommand(args) {
|
|
|
680
696
|
];
|
|
681
697
|
if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
682
698
|
|
|
683
|
-
// Ensure telemetry config exists (generates machine_id on first run)
|
|
684
|
-
ensureTelemetryConfig();
|
|
685
|
-
|
|
686
699
|
// Install bundled aw-usage producer hooks into ~/.claude.
|
|
687
700
|
// Copies the scripts + lib files and non-destructively merges 5 hook phases
|
|
688
701
|
// (SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop)
|
|
689
702
|
// into ~/.claude/settings.json. Idempotent. After this, every Claude Code
|
|
690
703
|
// session emits usage_events to the live telemetry API automatically.
|
|
691
|
-
|
|
692
|
-
try {
|
|
693
|
-
if (isDefaultRoutingEnabled(HOME, process.env)) {
|
|
694
|
-
const result = installAwUsageHooks();
|
|
695
|
-
awUsageHooksReport = formatAwUsageHooksInstallReport(result);
|
|
696
|
-
}
|
|
697
|
-
} catch (e) {
|
|
698
|
-
// Non-fatal — telemetry is observational. Surface the error in silent mode
|
|
699
|
-
// logs but don't block the install.
|
|
700
|
-
if (!silent) fmt.note(`aw-usage hooks install: ${e.message}`, 'Telemetry');
|
|
701
|
-
}
|
|
704
|
+
const awUsageHooksReport = installAwUsageHooksBestEffort({ silent });
|
|
702
705
|
|
|
703
706
|
// Write hook manifest after all hook installation is complete, including
|
|
704
707
|
// bundled usage hooks, so `aw nuke` can prune AW-managed settings entries.
|
|
@@ -11,15 +11,23 @@
|
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
buildEvent,
|
|
16
|
+
sendAsync,
|
|
17
|
+
isDisabled,
|
|
18
|
+
findRecentSdlcSessionForProject,
|
|
19
|
+
} = require('../lib/aw-usage-telemetry');
|
|
15
20
|
|
|
16
21
|
function buildCommitCreatedEvent({ commitHash = 'unknown', branch = 'unknown', cwd = process.cwd() } = {}) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const linkedSession = findRecentSdlcSessionForProject(cwd);
|
|
23
|
+
const event = buildEvent({
|
|
24
|
+
cwd,
|
|
25
|
+
...(linkedSession?.session_id ? { session_id: linkedSession.session_id } : {}),
|
|
26
|
+
}, 'commit_created', {
|
|
20
27
|
commit_hash: commitHash,
|
|
21
28
|
commit_sha: commitHash,
|
|
22
29
|
branch,
|
|
30
|
+
...(linkedSession?.session_id ? { linked_session_source: 'project_recent_sdlc_session' } : {}),
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
// Override harness to 'git' since this fires from a git hook, not a harness.
|
|
@@ -109,7 +109,9 @@ function processPromptSubmitInput(input, deps = {}) {
|
|
|
109
109
|
|
|
110
110
|
if (slash) {
|
|
111
111
|
persistSkill(getSessionId(input), input?.turn_id || null, slash);
|
|
112
|
-
persistSlashCmd(getSessionId(input), slash
|
|
112
|
+
persistSlashCmd(getSessionId(input), slash, {
|
|
113
|
+
cwd: input?.cwd || (input?.workspace_roots && input.workspace_roots[0]) || null,
|
|
114
|
+
});
|
|
113
115
|
events.push({
|
|
114
116
|
eventType: 'skill_invoked',
|
|
115
117
|
payload: {
|
|
@@ -21,6 +21,7 @@ const SENDER_SCRIPT = path.join(__dirname, '..', 'hooks', 'aw-usage-telemetry-se
|
|
|
21
21
|
const AW_HOME = path.join(os.homedir(), '.aw');
|
|
22
22
|
const CONFIG_PATH = path.join(AW_HOME, 'telemetry', 'config.json');
|
|
23
23
|
const SESSION_DIR = path.join(AW_HOME, 'telemetry', 'sessions');
|
|
24
|
+
const SESSION_PROJECT_DIR = path.join(SESSION_DIR, 'by-project');
|
|
24
25
|
const DEDUPE_DIR = path.join(os.tmpdir(), 'aw-usage-telemetry-dedupe');
|
|
25
26
|
|
|
26
27
|
// ── Git config cache (once per process) ──────────────────────────────
|
|
@@ -157,6 +158,16 @@ function computeProjectHash(cwd) {
|
|
|
157
158
|
return crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 16);
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
function normalizeProjectHash(value) {
|
|
162
|
+
return typeof value === 'string' && /^[a-f0-9]{16}$/i.test(value) ? value.toLowerCase() : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveProjectHash(context) {
|
|
166
|
+
if (!context || typeof context !== 'object') return null;
|
|
167
|
+
return normalizeProjectHash(context.project_hash || context.projectHash)
|
|
168
|
+
|| computeProjectHash(context.cwd);
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
// ── Session file cleanup ─────────────────────────────────────────────
|
|
161
172
|
// Prune session files older than SESSION_MAX_AGE_MS to prevent unbounded growth.
|
|
162
173
|
// Called once per session start — best-effort, never blocks.
|
|
@@ -241,21 +252,40 @@ function readSessionSkill(sessionId, turnId) {
|
|
|
241
252
|
// to the originating /aw:* invocation. Separate from `last_skill` because
|
|
242
253
|
// the source is the prompt, not the tool, and the lifetime is the whole
|
|
243
254
|
// session (not just one turn).
|
|
244
|
-
function
|
|
255
|
+
function writeProjectSessionIndex(projectHash, sessionId, slashCommand) {
|
|
256
|
+
if (!projectHash || !sessionId || !slashCommand?.is_sdlc_stage) return;
|
|
257
|
+
try {
|
|
258
|
+
fs.mkdirSync(SESSION_PROJECT_DIR, { recursive: true });
|
|
259
|
+
fs.writeFileSync(path.join(SESSION_PROJECT_DIR, `${projectHash}.json`), JSON.stringify({
|
|
260
|
+
project_hash: projectHash,
|
|
261
|
+
session_id: sessionId,
|
|
262
|
+
command_namespace: slashCommand.command_namespace || null,
|
|
263
|
+
command_name: slashCommand.command_name,
|
|
264
|
+
is_sdlc_stage: Boolean(slashCommand.is_sdlc_stage),
|
|
265
|
+
updated_at: new Date().toISOString(),
|
|
266
|
+
}));
|
|
267
|
+
} catch { /* ignore */ }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function persistSessionSlashCommand(sessionId, slashCommand, context = {}) {
|
|
245
271
|
if (!sessionId || !slashCommand?.command_name) return;
|
|
246
272
|
try {
|
|
247
273
|
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
248
274
|
const state = readSessionState(sessionId);
|
|
275
|
+
const projectHash = resolveProjectHash(context) || state.project_hash || null;
|
|
249
276
|
fs.writeFileSync(path.join(SESSION_DIR, sessionId + '.json'), JSON.stringify({
|
|
250
277
|
...state,
|
|
278
|
+
...(projectHash ? { project_hash: projectHash } : {}),
|
|
251
279
|
last_slash_command: {
|
|
252
280
|
command_namespace: slashCommand.command_namespace || null,
|
|
253
281
|
command_name: slashCommand.command_name,
|
|
254
282
|
command_args: slashCommand.command_args || '',
|
|
255
283
|
is_sdlc_stage: Boolean(slashCommand.is_sdlc_stage),
|
|
284
|
+
...(projectHash ? { project_hash: projectHash } : {}),
|
|
256
285
|
updated_at: new Date().toISOString(),
|
|
257
286
|
},
|
|
258
287
|
}));
|
|
288
|
+
writeProjectSessionIndex(projectHash, sessionId, slashCommand);
|
|
259
289
|
} catch { /* ignore */ }
|
|
260
290
|
}
|
|
261
291
|
|
|
@@ -265,6 +295,59 @@ function readSessionLastSlashCommand(sessionId) {
|
|
|
265
295
|
return cmd;
|
|
266
296
|
}
|
|
267
297
|
|
|
298
|
+
function parseUpdatedAt(value) {
|
|
299
|
+
const millis = Date.parse(value || '');
|
|
300
|
+
return Number.isFinite(millis) ? millis : 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs) {
|
|
304
|
+
if (!candidate?.session_id) return false;
|
|
305
|
+
if (!candidate?.is_sdlc_stage) return false;
|
|
306
|
+
if (candidate.project_hash !== projectHash) return false;
|
|
307
|
+
const updatedAt = parseUpdatedAt(candidate.updated_at);
|
|
308
|
+
if (!updatedAt) return false;
|
|
309
|
+
return Date.now() - updatedAt <= maxAgeMs;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readProjectSessionIndex(projectHash, maxAgeMs) {
|
|
313
|
+
try {
|
|
314
|
+
const candidate = JSON.parse(fs.readFileSync(path.join(SESSION_PROJECT_DIR, `${projectHash}.json`), 'utf8'));
|
|
315
|
+
return isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs) ? candidate : null;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function findRecentSdlcSessionForProject(cwd, maxAgeMs = SESSION_MAX_AGE_MS) {
|
|
322
|
+
const projectHash = computeProjectHash(cwd);
|
|
323
|
+
if (!projectHash) return null;
|
|
324
|
+
|
|
325
|
+
const indexed = readProjectSessionIndex(projectHash, maxAgeMs);
|
|
326
|
+
if (indexed) return indexed;
|
|
327
|
+
|
|
328
|
+
let best = null;
|
|
329
|
+
try {
|
|
330
|
+
const entries = fs.readdirSync(SESSION_DIR);
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (!entry.endsWith('.json')) continue;
|
|
333
|
+
const sessionId = entry.slice(0, -'.json'.length);
|
|
334
|
+
const state = readSessionState(sessionId);
|
|
335
|
+
const cmd = state.last_slash_command;
|
|
336
|
+
const candidate = {
|
|
337
|
+
session_id: sessionId,
|
|
338
|
+
project_hash: cmd?.project_hash || state.project_hash || null,
|
|
339
|
+
is_sdlc_stage: Boolean(cmd?.is_sdlc_stage),
|
|
340
|
+
updated_at: cmd?.updated_at || state.updated_at || null,
|
|
341
|
+
};
|
|
342
|
+
if (!isRecentSdlcSessionCandidate(candidate, projectHash, maxAgeMs)) continue;
|
|
343
|
+
if (!best || parseUpdatedAt(candidate.updated_at) > parseUpdatedAt(best.updated_at)) {
|
|
344
|
+
best = candidate;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch { /* ignore */ }
|
|
348
|
+
return best;
|
|
349
|
+
}
|
|
350
|
+
|
|
268
351
|
// ── Short-TTL dedupe guards ──────────────────────────────────────────
|
|
269
352
|
|
|
270
353
|
function normalizeDedupePart(value) {
|
|
@@ -503,6 +586,7 @@ module.exports = {
|
|
|
503
586
|
readSessionSkill,
|
|
504
587
|
persistSessionSlashCommand,
|
|
505
588
|
readSessionLastSlashCommand,
|
|
589
|
+
findRecentSdlcSessionForProject,
|
|
506
590
|
readLastAssistantFromTranscript,
|
|
507
591
|
resolvePromptText,
|
|
508
592
|
getAwCliVersionDetails,
|