@ghl-ai/aw 0.1.37-beta.76 → 0.1.37-beta.78
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/hooks/capabilities/telemetry.mjs +228 -20
- package/integrate.mjs +14 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// MUST never throw unhandled. Callers (dispatch.mjs) catch, but we guard anyway.
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, rmSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
@@ -155,6 +155,13 @@ function normalizeCursorInput(input) {
|
|
|
155
155
|
|
|
156
156
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
157
157
|
|
|
158
|
+
function resolveSessionId(input) {
|
|
159
|
+
return input?.session_id
|
|
160
|
+
|| input?.conversation_id
|
|
161
|
+
|| process.env.CODEX_THREAD_ID
|
|
162
|
+
|| 'unknown';
|
|
163
|
+
}
|
|
164
|
+
|
|
158
165
|
function resolveApiUrl() {
|
|
159
166
|
if (process.env.AW_API_URL) return `${process.env.AW_API_URL}/telemetry/ingest`;
|
|
160
167
|
const configPath = join(HOME, '.aw', 'config.json');
|
|
@@ -191,12 +198,71 @@ function resolveNamespace() {
|
|
|
191
198
|
} catch { return null; }
|
|
192
199
|
}
|
|
193
200
|
|
|
194
|
-
function detectPlatform() {
|
|
201
|
+
function detectPlatform(input) {
|
|
202
|
+
if (input?.hook_event_name || input?.cursor_version || input?.conversation_id) return 'cursor';
|
|
203
|
+
if (input?.platform) return input.platform;
|
|
204
|
+
if (input?.transcript_path) return 'claude-code';
|
|
205
|
+
if (input?.session_id && !process.env.CODEX_THREAD_ID) return 'claude-code';
|
|
195
206
|
if (process.env.CLAUDECODE) return 'claude-code';
|
|
196
207
|
if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_PROJECT_DIR) return 'cursor';
|
|
197
208
|
if (Object.keys(process.env).some(k => k.startsWith('WINDSURF_'))) return 'windsurf';
|
|
198
209
|
if (Object.keys(process.env).some(k => k.startsWith('CODEX_'))) return 'codex';
|
|
199
|
-
return 'claude-code';
|
|
210
|
+
return 'claude-code';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function findCodexTranscriptPath(sessionId) {
|
|
214
|
+
if (!sessionId || sessionId === 'unknown') return null;
|
|
215
|
+
|
|
216
|
+
const sessionsRoot = join(HOME, '.codex', 'sessions');
|
|
217
|
+
if (!existsSync(sessionsRoot)) return null;
|
|
218
|
+
|
|
219
|
+
const stack = [sessionsRoot];
|
|
220
|
+
let newestMatch = null;
|
|
221
|
+
let newestStamp = 0;
|
|
222
|
+
|
|
223
|
+
while (stack.length > 0) {
|
|
224
|
+
const dir = stack.pop();
|
|
225
|
+
let entries = [];
|
|
226
|
+
try {
|
|
227
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
228
|
+
} catch {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
const fullPath = join(dir, entry.name);
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
stack.push(fullPath);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
|
|
239
|
+
if (!entry.name.includes(sessionId)) continue;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const stamp = statSync(fullPath).mtimeMs;
|
|
243
|
+
if (Number.isFinite(stamp) && stamp >= newestStamp) {
|
|
244
|
+
newestStamp = stamp;
|
|
245
|
+
newestMatch = fullPath;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
newestMatch = newestMatch || fullPath;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return newestMatch;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function classifyError(errStr) {
|
|
257
|
+
if (!errStr) return { errorType: null, errorMessage: null };
|
|
258
|
+
const errorMessage = String(errStr).slice(0, 1024);
|
|
259
|
+
let errorType = 'unknown';
|
|
260
|
+
if (/rate.?limit|429|too many requests/i.test(errorMessage)) errorType = 'rate_limit';
|
|
261
|
+
else if (/context.?(overflow|length|window|limit|too long)/i.test(errorMessage)) errorType = 'context_overflow';
|
|
262
|
+
else if (/timeout|timed?\s*out|ETIMEDOUT/i.test(errorMessage)) errorType = 'timeout';
|
|
263
|
+
else if (/cancel|abort|interrupt/i.test(errorMessage)) errorType = 'user_cancel';
|
|
264
|
+
else if (/tool.?(fail|error)|execution.?error|process exited with code [1-9]/i.test(errorMessage)) errorType = 'tool_failure';
|
|
265
|
+
return { errorType, errorMessage };
|
|
200
266
|
}
|
|
201
267
|
|
|
202
268
|
function computeProjectHash(cwd) {
|
|
@@ -360,14 +426,7 @@ function extractFromTranscript(transcriptPath) {
|
|
|
360
426
|
const entryError = entry.error || entry.message?.error;
|
|
361
427
|
if (entryError) {
|
|
362
428
|
const errStr = typeof entryError === 'string' ? entryError : (entryError.message || JSON.stringify(entryError));
|
|
363
|
-
errorMessage = errStr
|
|
364
|
-
// Classify error type
|
|
365
|
-
if (/rate.?limit|429|too many requests/i.test(errStr)) errorType = 'rate_limit';
|
|
366
|
-
else if (/context.?(overflow|length|window|limit|too long)/i.test(errStr)) errorType = 'context_overflow';
|
|
367
|
-
else if (/timeout|timed?\s*out|ETIMEDOUT/i.test(errStr)) errorType = 'timeout';
|
|
368
|
-
else if (/cancel|abort|interrupt/i.test(errStr)) errorType = 'user_cancel';
|
|
369
|
-
else if (/tool.?(fail|error)|execution.?error/i.test(errStr)) errorType = 'tool_failure';
|
|
370
|
-
else errorType = 'unknown';
|
|
429
|
+
({ errorType, errorMessage } = classifyError(errStr));
|
|
371
430
|
}
|
|
372
431
|
} catch { /* skip malformed lines */ }
|
|
373
432
|
}
|
|
@@ -416,6 +475,129 @@ function extractFromTranscript(transcriptPath) {
|
|
|
416
475
|
}
|
|
417
476
|
}
|
|
418
477
|
|
|
478
|
+
function extractFromCodexSession(transcriptPath) {
|
|
479
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return null;
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const content = readFileSync(transcriptPath, 'utf8');
|
|
483
|
+
const lines = content.split('\n').filter(Boolean);
|
|
484
|
+
|
|
485
|
+
let lastInputTokens = 0;
|
|
486
|
+
let lastOutputTokens = 0;
|
|
487
|
+
let lastCacheCreation = 0;
|
|
488
|
+
let lastCacheRead = 0;
|
|
489
|
+
let model = null;
|
|
490
|
+
let command = null;
|
|
491
|
+
let cwd = null;
|
|
492
|
+
let platformVersion = null;
|
|
493
|
+
const agentsUsed = new Set();
|
|
494
|
+
const skillsApplied = new Set();
|
|
495
|
+
const mcpToolsUsed = new Set();
|
|
496
|
+
const prUrls = new Set();
|
|
497
|
+
let toolTotal = 0;
|
|
498
|
+
let toolFailed = 0;
|
|
499
|
+
let errorType = null;
|
|
500
|
+
let errorMessage = null;
|
|
501
|
+
|
|
502
|
+
for (const line of lines) {
|
|
503
|
+
let entry;
|
|
504
|
+
try {
|
|
505
|
+
entry = JSON.parse(line);
|
|
506
|
+
} catch {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (entry.type === 'session_meta') {
|
|
511
|
+
const meta = entry.payload || {};
|
|
512
|
+
cwd = meta.cwd || cwd;
|
|
513
|
+
platformVersion = meta.cli_version || platformVersion;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count') {
|
|
517
|
+
const totals = entry.payload?.info?.total_token_usage || {};
|
|
518
|
+
lastInputTokens = totals.input_tokens || 0;
|
|
519
|
+
lastOutputTokens = (totals.output_tokens || 0) + (totals.reasoning_output_tokens || 0);
|
|
520
|
+
lastCacheRead = totals.cached_input_tokens || 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (entry.type === 'response_item') {
|
|
524
|
+
const payload = entry.payload || {};
|
|
525
|
+
if (payload.type === 'function_call') {
|
|
526
|
+
const name = payload.name || '';
|
|
527
|
+
const argsRaw = typeof payload.arguments === 'string' ? payload.arguments : JSON.stringify(payload.arguments || {});
|
|
528
|
+
|
|
529
|
+
if (name) toolTotal++;
|
|
530
|
+
if (name === 'spawn_agent') {
|
|
531
|
+
try {
|
|
532
|
+
const parsed = JSON.parse(argsRaw || '{}');
|
|
533
|
+
const agentType = parsed.agent_type || parsed.model || 'spawn_agent';
|
|
534
|
+
agentsUsed.add(String(agentType).slice(0, 128));
|
|
535
|
+
} catch {
|
|
536
|
+
agentsUsed.add('spawn_agent');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (name === 'Skill') {
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(argsRaw || '{}');
|
|
542
|
+
if (parsed.skill) {
|
|
543
|
+
command = String(parsed.skill).slice(0, 128);
|
|
544
|
+
skillsApplied.add(command);
|
|
545
|
+
}
|
|
546
|
+
} catch { /* ignore */ }
|
|
547
|
+
}
|
|
548
|
+
if (name.startsWith('mcp__')) {
|
|
549
|
+
mcpToolsUsed.add(name.slice(0, 128));
|
|
550
|
+
}
|
|
551
|
+
scanForPrUrls(argsRaw, prUrls);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (payload.type === 'function_call_output') {
|
|
555
|
+
const outputText = String(payload.output || '');
|
|
556
|
+
scanForPrUrls(outputText, prUrls);
|
|
557
|
+
const failureMatch = outputText.match(/Process exited with code\s+(\d+)/i);
|
|
558
|
+
if (failureMatch) {
|
|
559
|
+
const exitCode = Number.parseInt(failureMatch[1], 10);
|
|
560
|
+
if (Number.isFinite(exitCode) && exitCode !== 0) {
|
|
561
|
+
toolFailed++;
|
|
562
|
+
({ errorType, errorMessage } = classifyError(outputText));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (payload.type === 'message') {
|
|
568
|
+
const textContent = Array.isArray(payload.content)
|
|
569
|
+
? payload.content.map(part => part?.text || '').join('\n')
|
|
570
|
+
: '';
|
|
571
|
+
scanForPrUrls(textContent, prUrls);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
inputTokens: lastInputTokens,
|
|
578
|
+
outputTokens: lastOutputTokens,
|
|
579
|
+
cacheCreation: lastCacheCreation,
|
|
580
|
+
cacheRead: lastCacheRead,
|
|
581
|
+
totalTokens: lastInputTokens + lastOutputTokens + lastCacheCreation + lastCacheRead,
|
|
582
|
+
model,
|
|
583
|
+
command,
|
|
584
|
+
cwd,
|
|
585
|
+
platformVersion,
|
|
586
|
+
agentsUsed: [...agentsUsed],
|
|
587
|
+
skillsApplied: [...skillsApplied],
|
|
588
|
+
mcpToolsUsed: [...mcpToolsUsed],
|
|
589
|
+
toolTotal,
|
|
590
|
+
toolPassed: Math.max(0, toolTotal - toolFailed),
|
|
591
|
+
toolFailed,
|
|
592
|
+
prUrls: [...prUrls],
|
|
593
|
+
errorType,
|
|
594
|
+
errorMessage,
|
|
595
|
+
};
|
|
596
|
+
} catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
419
601
|
/**
|
|
420
602
|
* Estimate token counts from Cursor's inline response text.
|
|
421
603
|
* Cursor doesn't provide token usage, so we estimate ~4 chars/token.
|
|
@@ -646,8 +828,14 @@ async function pushPerfSummaries(cwd, transcript, deltaTotal, deltaCost, model,
|
|
|
646
828
|
*/
|
|
647
829
|
export async function handleSessionStart(rawInput) {
|
|
648
830
|
const input = normalizeCursorInput(rawInput);
|
|
649
|
-
const sessionId = input
|
|
650
|
-
const
|
|
831
|
+
const sessionId = resolveSessionId(input);
|
|
832
|
+
const codexTranscriptPath = detectPlatform(input) === 'codex'
|
|
833
|
+
? findCodexTranscriptPath(sessionId)
|
|
834
|
+
: null;
|
|
835
|
+
const codexTranscript = codexTranscriptPath
|
|
836
|
+
? extractFromCodexSession(codexTranscriptPath)
|
|
837
|
+
: null;
|
|
838
|
+
const cwd = input.cwd || codexTranscript?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
651
839
|
|
|
652
840
|
// Refresh pricing from LiteLLM if stale (>24h). Non-blocking — if it fails,
|
|
653
841
|
// existing cached pricing or hardcoded fallback is used.
|
|
@@ -657,11 +845,11 @@ export async function handleSessionStart(rawInput) {
|
|
|
657
845
|
session_id: sessionId,
|
|
658
846
|
branch: getBranch(cwd),
|
|
659
847
|
project_hash: computeProjectHash(cwd),
|
|
660
|
-
platform: detectPlatform(),
|
|
661
|
-
platform_version: input.platform_version || null,
|
|
848
|
+
platform: detectPlatform(rawInput),
|
|
849
|
+
platform_version: input.platform_version || codexTranscript?.platformVersion || null,
|
|
662
850
|
namespace: resolveNamespace(),
|
|
663
851
|
start_ts: new Date().toISOString(),
|
|
664
|
-
model: input.model || null,
|
|
852
|
+
model: input.model || codexTranscript?.model || null,
|
|
665
853
|
cwd,
|
|
666
854
|
entry_id: 0, // monotonic counter for costs.jsonl entries
|
|
667
855
|
last_flushed_entry: 0, // checkpoint: last entry_id pushed to API
|
|
@@ -681,17 +869,25 @@ export async function handleSessionStart(rawInput) {
|
|
|
681
869
|
*/
|
|
682
870
|
export async function handleStop(rawInput) {
|
|
683
871
|
const input = normalizeCursorInput(rawInput);
|
|
684
|
-
const sessionId = input
|
|
685
|
-
const
|
|
872
|
+
const sessionId = resolveSessionId(input);
|
|
873
|
+
const platform = detectPlatform(rawInput);
|
|
874
|
+
const transcriptPath = input.transcript_path
|
|
875
|
+
|| process.env.CURSOR_TRANSCRIPT_PATH
|
|
876
|
+
|| (platform === 'codex' ? findCodexTranscriptPath(sessionId) : null);
|
|
686
877
|
const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
687
878
|
const status = input.status || 'completed';
|
|
688
879
|
const stdinModel = input.model || null;
|
|
689
880
|
|
|
690
881
|
// For Cursor: if no transcript file, estimate from inline text
|
|
691
|
-
let transcript =
|
|
882
|
+
let transcript = platform === 'codex'
|
|
883
|
+
? extractFromCodexSession(transcriptPath)
|
|
884
|
+
: extractFromTranscript(transcriptPath);
|
|
692
885
|
if (!transcript && input._cursor_text) {
|
|
693
886
|
transcript = estimateFromCursorText(input._cursor_text, stdinModel);
|
|
694
887
|
}
|
|
888
|
+
if (!transcript && platform === 'codex' && transcriptPath) {
|
|
889
|
+
transcript = extractFromCodexSession(transcriptPath);
|
|
890
|
+
}
|
|
695
891
|
const model = stdinModel || transcript?.model || 'unknown';
|
|
696
892
|
|
|
697
893
|
// ── Token delta via snapshot diffing ─────────────────────────────────────
|
|
@@ -725,6 +921,16 @@ export async function handleStop(rawInput) {
|
|
|
725
921
|
|
|
726
922
|
// ── Append to costs.jsonl with incrementing entry_id ───────────────────
|
|
727
923
|
let session = readSession(sessionId);
|
|
924
|
+
if (!session && platform === 'codex') {
|
|
925
|
+
await handleSessionStart({
|
|
926
|
+
...input,
|
|
927
|
+
session_id: sessionId,
|
|
928
|
+
cwd: input.cwd || transcript?.cwd || cwd,
|
|
929
|
+
model: stdinModel || transcript?.model || null,
|
|
930
|
+
platform_version: input.platform_version || transcript?.platformVersion || null,
|
|
931
|
+
});
|
|
932
|
+
session = readSession(sessionId);
|
|
933
|
+
}
|
|
728
934
|
const entryId = (session?.entry_id || 0) + 1;
|
|
729
935
|
|
|
730
936
|
try {
|
|
@@ -741,7 +947,7 @@ export async function handleStop(rawInput) {
|
|
|
741
947
|
total_tokens: deltaTotal,
|
|
742
948
|
cost_usd: Math.round(deltaCost * 1_000_000) / 1_000_000,
|
|
743
949
|
status,
|
|
744
|
-
platform
|
|
950
|
+
platform,
|
|
745
951
|
};
|
|
746
952
|
appendFileSync(costPath, JSON.stringify(record) + '\n');
|
|
747
953
|
} catch { /* never block */ }
|
|
@@ -750,6 +956,8 @@ export async function handleStop(rawInput) {
|
|
|
750
956
|
if (session) {
|
|
751
957
|
session.entry_id = entryId;
|
|
752
958
|
session.model = model !== 'unknown' ? model : session.model;
|
|
959
|
+
session.platform_version = input.platform_version || transcript?.platformVersion || session.platform_version;
|
|
960
|
+
session.cwd = transcript?.cwd || session.cwd;
|
|
753
961
|
|
|
754
962
|
// Merge PR URLs
|
|
755
963
|
if (transcript?.prUrls?.length) {
|
package/integrate.mjs
CHANGED
|
@@ -681,6 +681,20 @@ export function installIdeHooks() {
|
|
|
681
681
|
timeoutSec: timeout,
|
|
682
682
|
statusMessage: `AW telemetry (${event})`,
|
|
683
683
|
});
|
|
684
|
+
|
|
685
|
+
// Clean up legacy top-level event wrappers from earlier installs so Codex
|
|
686
|
+
// has a single canonical hook definition and we avoid duplicate execution.
|
|
687
|
+
if (Array.isArray(codexHooksFile[event])) {
|
|
688
|
+
const cleaned = codexHooksFile[event]
|
|
689
|
+
.map(group => {
|
|
690
|
+
if (!group || !Array.isArray(group.hooks)) return group;
|
|
691
|
+
const hooks = group.hooks.filter(h => !h.command?.includes('.aw/hooks/'));
|
|
692
|
+
return hooks.length > 0 ? { ...group, hooks } : null;
|
|
693
|
+
})
|
|
694
|
+
.filter(Boolean);
|
|
695
|
+
if (cleaned.length > 0) codexHooksFile[event] = cleaned;
|
|
696
|
+
else delete codexHooksFile[event];
|
|
697
|
+
}
|
|
684
698
|
}
|
|
685
699
|
|
|
686
700
|
writeFileSync(codexHooksPath, JSON.stringify(codexHooksFile, null, 2) + '\n');
|