@ghl-ai/aw 0.1.37-beta.77 → 0.1.37-beta.79
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 +224 -17
- package/integrate.mjs +32 -7
- 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');
|
|
@@ -193,6 +200,9 @@ function resolveNamespace() {
|
|
|
193
200
|
|
|
194
201
|
function detectPlatform(input) {
|
|
195
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';
|
|
196
206
|
if (process.env.CLAUDECODE) return 'claude-code';
|
|
197
207
|
if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_PROJECT_DIR) return 'cursor';
|
|
198
208
|
if (Object.keys(process.env).some(k => k.startsWith('WINDSURF_'))) return 'windsurf';
|
|
@@ -200,6 +210,61 @@ function detectPlatform(input) {
|
|
|
200
210
|
return 'claude-code';
|
|
201
211
|
}
|
|
202
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 };
|
|
266
|
+
}
|
|
267
|
+
|
|
203
268
|
function computeProjectHash(cwd) {
|
|
204
269
|
try {
|
|
205
270
|
const remote = execSync('git remote get-url origin 2>/dev/null', {
|
|
@@ -361,14 +426,7 @@ function extractFromTranscript(transcriptPath) {
|
|
|
361
426
|
const entryError = entry.error || entry.message?.error;
|
|
362
427
|
if (entryError) {
|
|
363
428
|
const errStr = typeof entryError === 'string' ? entryError : (entryError.message || JSON.stringify(entryError));
|
|
364
|
-
errorMessage = errStr
|
|
365
|
-
// Classify error type
|
|
366
|
-
if (/rate.?limit|429|too many requests/i.test(errStr)) errorType = 'rate_limit';
|
|
367
|
-
else if (/context.?(overflow|length|window|limit|too long)/i.test(errStr)) errorType = 'context_overflow';
|
|
368
|
-
else if (/timeout|timed?\s*out|ETIMEDOUT/i.test(errStr)) errorType = 'timeout';
|
|
369
|
-
else if (/cancel|abort|interrupt/i.test(errStr)) errorType = 'user_cancel';
|
|
370
|
-
else if (/tool.?(fail|error)|execution.?error/i.test(errStr)) errorType = 'tool_failure';
|
|
371
|
-
else errorType = 'unknown';
|
|
429
|
+
({ errorType, errorMessage } = classifyError(errStr));
|
|
372
430
|
}
|
|
373
431
|
} catch { /* skip malformed lines */ }
|
|
374
432
|
}
|
|
@@ -417,6 +475,129 @@ function extractFromTranscript(transcriptPath) {
|
|
|
417
475
|
}
|
|
418
476
|
}
|
|
419
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
|
+
|
|
420
601
|
/**
|
|
421
602
|
* Estimate token counts from Cursor's inline response text.
|
|
422
603
|
* Cursor doesn't provide token usage, so we estimate ~4 chars/token.
|
|
@@ -647,8 +828,14 @@ async function pushPerfSummaries(cwd, transcript, deltaTotal, deltaCost, model,
|
|
|
647
828
|
*/
|
|
648
829
|
export async function handleSessionStart(rawInput) {
|
|
649
830
|
const input = normalizeCursorInput(rawInput);
|
|
650
|
-
const sessionId = input
|
|
651
|
-
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();
|
|
652
839
|
|
|
653
840
|
// Refresh pricing from LiteLLM if stale (>24h). Non-blocking — if it fails,
|
|
654
841
|
// existing cached pricing or hardcoded fallback is used.
|
|
@@ -659,10 +846,10 @@ export async function handleSessionStart(rawInput) {
|
|
|
659
846
|
branch: getBranch(cwd),
|
|
660
847
|
project_hash: computeProjectHash(cwd),
|
|
661
848
|
platform: detectPlatform(rawInput),
|
|
662
|
-
platform_version: input.platform_version || null,
|
|
849
|
+
platform_version: input.platform_version || codexTranscript?.platformVersion || null,
|
|
663
850
|
namespace: resolveNamespace(),
|
|
664
851
|
start_ts: new Date().toISOString(),
|
|
665
|
-
model: input.model || null,
|
|
852
|
+
model: input.model || codexTranscript?.model || null,
|
|
666
853
|
cwd,
|
|
667
854
|
entry_id: 0, // monotonic counter for costs.jsonl entries
|
|
668
855
|
last_flushed_entry: 0, // checkpoint: last entry_id pushed to API
|
|
@@ -682,17 +869,25 @@ export async function handleSessionStart(rawInput) {
|
|
|
682
869
|
*/
|
|
683
870
|
export async function handleStop(rawInput) {
|
|
684
871
|
const input = normalizeCursorInput(rawInput);
|
|
685
|
-
const sessionId = input
|
|
686
|
-
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);
|
|
687
877
|
const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
688
878
|
const status = input.status || 'completed';
|
|
689
879
|
const stdinModel = input.model || null;
|
|
690
880
|
|
|
691
881
|
// For Cursor: if no transcript file, estimate from inline text
|
|
692
|
-
let transcript =
|
|
882
|
+
let transcript = platform === 'codex'
|
|
883
|
+
? extractFromCodexSession(transcriptPath)
|
|
884
|
+
: extractFromTranscript(transcriptPath);
|
|
693
885
|
if (!transcript && input._cursor_text) {
|
|
694
886
|
transcript = estimateFromCursorText(input._cursor_text, stdinModel);
|
|
695
887
|
}
|
|
888
|
+
if (!transcript && platform === 'codex' && transcriptPath) {
|
|
889
|
+
transcript = extractFromCodexSession(transcriptPath);
|
|
890
|
+
}
|
|
696
891
|
const model = stdinModel || transcript?.model || 'unknown';
|
|
697
892
|
|
|
698
893
|
// ── Token delta via snapshot diffing ─────────────────────────────────────
|
|
@@ -726,6 +921,16 @@ export async function handleStop(rawInput) {
|
|
|
726
921
|
|
|
727
922
|
// ── Append to costs.jsonl with incrementing entry_id ───────────────────
|
|
728
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
|
+
}
|
|
729
934
|
const entryId = (session?.entry_id || 0) + 1;
|
|
730
935
|
|
|
731
936
|
try {
|
|
@@ -742,7 +947,7 @@ export async function handleStop(rawInput) {
|
|
|
742
947
|
total_tokens: deltaTotal,
|
|
743
948
|
cost_usd: Math.round(deltaCost * 1_000_000) / 1_000_000,
|
|
744
949
|
status,
|
|
745
|
-
platform
|
|
950
|
+
platform,
|
|
746
951
|
};
|
|
747
952
|
appendFileSync(costPath, JSON.stringify(record) + '\n');
|
|
748
953
|
} catch { /* never block */ }
|
|
@@ -751,6 +956,8 @@ export async function handleStop(rawInput) {
|
|
|
751
956
|
if (session) {
|
|
752
957
|
session.entry_id = entryId;
|
|
753
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;
|
|
754
961
|
|
|
755
962
|
// Merge PR URLs
|
|
756
963
|
if (transcript?.prUrls?.length) {
|
package/integrate.mjs
CHANGED
|
@@ -669,18 +669,43 @@ export function installIdeHooks() {
|
|
|
669
669
|
for (const [event, { cmd, timeout }] of Object.entries(codexEvents)) {
|
|
670
670
|
if (!codexHooksFile.hooks[event]) codexHooksFile.hooks[event] = [];
|
|
671
671
|
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
);
|
|
672
|
+
// Codex expects grouped hook entries:
|
|
673
|
+
// { matcher?, hooks: [{ type, command, timeoutSec, statusMessage }] }
|
|
674
|
+
// Preserve non-AW groups and non-AW hooks inside mixed groups.
|
|
675
|
+
const existingGroups = Array.isArray(codexHooksFile.hooks[event]) ? codexHooksFile.hooks[event] : [];
|
|
676
|
+
const preservedGroups = existingGroups
|
|
677
|
+
.map(group => {
|
|
678
|
+
if (!group || !Array.isArray(group.hooks)) return null;
|
|
679
|
+
const hooks = group.hooks.filter(h => !h.command?.includes('.aw/hooks/'));
|
|
680
|
+
return hooks.length > 0 ? { ...group, hooks } : null;
|
|
681
|
+
})
|
|
682
|
+
.filter(Boolean);
|
|
676
683
|
|
|
677
|
-
|
|
678
|
-
codexHooksFile.hooks[event].push({
|
|
684
|
+
const awHook = {
|
|
679
685
|
type: 'command',
|
|
680
686
|
command: cmd,
|
|
681
687
|
timeoutSec: timeout,
|
|
682
688
|
statusMessage: `AW telemetry (${event})`,
|
|
683
|
-
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const awGroup = event === 'SessionStart'
|
|
692
|
+
? { matcher: 'startup', hooks: [awHook] }
|
|
693
|
+
: { hooks: [awHook] };
|
|
694
|
+
|
|
695
|
+
codexHooksFile.hooks[event] = [...preservedGroups, awGroup];
|
|
696
|
+
|
|
697
|
+
// Clean up incorrect top-level event wrappers from prior installs.
|
|
698
|
+
if (Array.isArray(codexHooksFile[event])) {
|
|
699
|
+
const cleaned = codexHooksFile[event]
|
|
700
|
+
.map(group => {
|
|
701
|
+
if (!group || !Array.isArray(group.hooks)) return null;
|
|
702
|
+
const hooks = group.hooks.filter(h => !h.command?.includes('.aw/hooks/'));
|
|
703
|
+
return hooks.length > 0 ? { ...group, hooks } : null;
|
|
704
|
+
})
|
|
705
|
+
.filter(Boolean);
|
|
706
|
+
if (cleaned.length > 0) codexHooksFile[event] = cleaned;
|
|
707
|
+
else delete codexHooksFile[event];
|
|
708
|
+
}
|
|
684
709
|
}
|
|
685
710
|
|
|
686
711
|
writeFileSync(codexHooksPath, JSON.stringify(codexHooksFile, null, 2) + '\n');
|