@ghl-ai/aw 0.1.37-beta.74 → 0.1.37-beta.76
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 +182 -11
- package/hooks/manifest.mjs +5 -4
- package/hooks/shared/after-agent-response.mjs +5 -0
- package/integrate.mjs +19 -14
- package/package.json +1 -1
|
@@ -124,6 +124,35 @@ function loadPricing() {
|
|
|
124
124
|
// Loaded once per process (each hook event is a separate node invocation)
|
|
125
125
|
const PRICING = loadPricing();
|
|
126
126
|
|
|
127
|
+
// ── Cursor stdin normalizer ──────────────────────────────────────────────────
|
|
128
|
+
// Cursor sends a different JSON shape than Claude Code. This function normalizes
|
|
129
|
+
// it to the format our handlers expect, so all handlers work uniformly.
|
|
130
|
+
//
|
|
131
|
+
// Cursor stdin shape:
|
|
132
|
+
// { conversation_id, generation_id, model, text, hook_event_name,
|
|
133
|
+
// cursor_version, workspace_roots: ["/path/..."], attachments? }
|
|
134
|
+
//
|
|
135
|
+
// Claude Code stdin shape:
|
|
136
|
+
// { session_id, transcript_path, cwd, model, status, ... }
|
|
137
|
+
|
|
138
|
+
function normalizeCursorInput(input) {
|
|
139
|
+
if (!input.conversation_id && !input.hook_event_name) return input;
|
|
140
|
+
|
|
141
|
+
const cwd = input.workspace_roots?.[0]
|
|
142
|
+
|| process.env.CURSOR_PROJECT_DIR
|
|
143
|
+
|| process.cwd();
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
...input,
|
|
147
|
+
session_id: input.conversation_id || input.session_id,
|
|
148
|
+
cwd,
|
|
149
|
+
model: (input.model && input.model !== 'default') ? input.model : null,
|
|
150
|
+
platform_version: input.cursor_version || input.platform_version || null,
|
|
151
|
+
_cursor_text: input.text || null,
|
|
152
|
+
_cursor_generation_id: input.generation_id || null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
128
157
|
|
|
129
158
|
function resolveApiUrl() {
|
|
@@ -387,6 +416,44 @@ function extractFromTranscript(transcriptPath) {
|
|
|
387
416
|
}
|
|
388
417
|
}
|
|
389
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Estimate token counts from Cursor's inline response text.
|
|
421
|
+
* Cursor doesn't provide token usage, so we estimate ~4 chars/token.
|
|
422
|
+
*/
|
|
423
|
+
function estimateFromCursorText(text, model) {
|
|
424
|
+
if (!text) return null;
|
|
425
|
+
const outputChars = text.length;
|
|
426
|
+
const outputTokens = Math.round(outputChars / 4);
|
|
427
|
+
const inputTokens = Math.round(outputTokens * 1.5); // rough estimate: inputs > outputs
|
|
428
|
+
|
|
429
|
+
const prUrls = new Set();
|
|
430
|
+
scanForPrUrls(text, prUrls);
|
|
431
|
+
|
|
432
|
+
const mcpToolsUsed = new Set();
|
|
433
|
+
const mcpRe = /mcp__\w+/g;
|
|
434
|
+
const mcpMatches = text.match(mcpRe);
|
|
435
|
+
if (mcpMatches) for (const m of mcpMatches) mcpToolsUsed.add(m.slice(0, 128));
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
inputTokens,
|
|
439
|
+
outputTokens,
|
|
440
|
+
cacheCreation: 0,
|
|
441
|
+
cacheRead: 0,
|
|
442
|
+
totalTokens: inputTokens + outputTokens,
|
|
443
|
+
model: model || null,
|
|
444
|
+
command: null,
|
|
445
|
+
agentsUsed: [],
|
|
446
|
+
skillsApplied: [],
|
|
447
|
+
mcpToolsUsed: [...mcpToolsUsed],
|
|
448
|
+
toolTotal: 0,
|
|
449
|
+
toolPassed: 0,
|
|
450
|
+
toolFailed: 0,
|
|
451
|
+
prUrls: [...prUrls],
|
|
452
|
+
errorType: null,
|
|
453
|
+
errorMessage: null,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
390
457
|
/** Scan text for GitHub PR URLs */
|
|
391
458
|
function scanForPrUrls(text, prUrlSet) {
|
|
392
459
|
if (!text) return;
|
|
@@ -577,9 +644,10 @@ async function pushPerfSummaries(cwd, transcript, deltaTotal, deltaCost, model,
|
|
|
577
644
|
/**
|
|
578
645
|
* SessionStart — write session metadata. No network.
|
|
579
646
|
*/
|
|
580
|
-
export async function handleSessionStart(
|
|
581
|
-
const
|
|
582
|
-
const
|
|
647
|
+
export async function handleSessionStart(rawInput) {
|
|
648
|
+
const input = normalizeCursorInput(rawInput);
|
|
649
|
+
const sessionId = input.session_id || `s-${Date.now()}`;
|
|
650
|
+
const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
583
651
|
|
|
584
652
|
// Refresh pricing from LiteLLM if stale (>24h). Non-blocking — if it fails,
|
|
585
653
|
// existing cached pricing or hardcoded fallback is used.
|
|
@@ -611,14 +679,19 @@ export async function handleSessionStart(input) {
|
|
|
611
679
|
* Stop — read transcript, compute token delta, append costs.jsonl. No network.
|
|
612
680
|
* Also scans for PR URLs and MCP tools.
|
|
613
681
|
*/
|
|
614
|
-
export async function handleStop(
|
|
615
|
-
const
|
|
682
|
+
export async function handleStop(rawInput) {
|
|
683
|
+
const input = normalizeCursorInput(rawInput);
|
|
684
|
+
const sessionId = input.session_id || 'unknown';
|
|
616
685
|
const transcriptPath = input.transcript_path || process.env.CURSOR_TRANSCRIPT_PATH || null;
|
|
617
|
-
const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.
|
|
686
|
+
const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
618
687
|
const status = input.status || 'completed';
|
|
619
688
|
const stdinModel = input.model || null;
|
|
620
689
|
|
|
621
|
-
|
|
690
|
+
// For Cursor: if no transcript file, estimate from inline text
|
|
691
|
+
let transcript = extractFromTranscript(transcriptPath);
|
|
692
|
+
if (!transcript && input._cursor_text) {
|
|
693
|
+
transcript = estimateFromCursorText(input._cursor_text, stdinModel);
|
|
694
|
+
}
|
|
622
695
|
const model = stdinModel || transcript?.model || 'unknown';
|
|
623
696
|
|
|
624
697
|
// ── Token delta via snapshot diffing ─────────────────────────────────────
|
|
@@ -719,12 +792,109 @@ export async function handleStop(input) {
|
|
|
719
792
|
await pushPerfSummaries(cwd, transcript, deltaTotal, deltaCost, model, input, session?.namespace);
|
|
720
793
|
}
|
|
721
794
|
|
|
795
|
+
/**
|
|
796
|
+
* AfterAgentResponse — Cursor-specific: fires after every agent response.
|
|
797
|
+
* Creates session if missing (Cursor's sessionStart is unreliable), then
|
|
798
|
+
* estimates tokens from the response text and appends to costs.jsonl.
|
|
799
|
+
* Identical logic to handleStop but triggered more reliably in Cursor.
|
|
800
|
+
*/
|
|
801
|
+
export async function handleAfterAgentResponse(rawInput) {
|
|
802
|
+
const input = normalizeCursorInput(rawInput);
|
|
803
|
+
const sessionId = input.session_id || 'unknown';
|
|
804
|
+
const cwd = input.cwd || process.cwd();
|
|
805
|
+
const stdinModel = input.model || null;
|
|
806
|
+
|
|
807
|
+
// Ensure session exists — Cursor's sessionStart hook can be flaky
|
|
808
|
+
let session = readSession(sessionId);
|
|
809
|
+
if (!session) {
|
|
810
|
+
await handleSessionStart(rawInput);
|
|
811
|
+
session = readSession(sessionId);
|
|
812
|
+
if (!session) return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Estimate tokens from Cursor's inline text
|
|
816
|
+
const transcript = input._cursor_text
|
|
817
|
+
? estimateFromCursorText(input._cursor_text, stdinModel)
|
|
818
|
+
: null;
|
|
819
|
+
if (!transcript) return; // no text content = nothing to track
|
|
820
|
+
|
|
821
|
+
const model = stdinModel || transcript.model || session.model || 'unknown';
|
|
822
|
+
|
|
823
|
+
// ── Token delta via snapshot diffing (same as handleStop) ──────────────
|
|
824
|
+
mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
825
|
+
const snapPath = snapshotPath(sessionId);
|
|
826
|
+
let prevSnapshot = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
827
|
+
if (existsSync(snapPath)) {
|
|
828
|
+
try { prevSnapshot = JSON.parse(readFileSync(snapPath, 'utf8')); } catch { /* corrupted */ }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// For afterAgentResponse, each event is a new response — use absolute values as delta
|
|
832
|
+
const deltaInput = transcript.inputTokens || 0;
|
|
833
|
+
const deltaOutput = transcript.outputTokens || 0;
|
|
834
|
+
const deltaCacheCreation = 0;
|
|
835
|
+
const deltaCacheRead = 0;
|
|
836
|
+
const deltaTotal = deltaInput + deltaOutput;
|
|
837
|
+
const deltaCost = estimateCost(model, deltaInput, deltaOutput, deltaCacheCreation, deltaCacheRead);
|
|
838
|
+
|
|
839
|
+
// Update snapshot with cumulative totals
|
|
840
|
+
try {
|
|
841
|
+
writeFileSync(snapPath, JSON.stringify({
|
|
842
|
+
input: (prevSnapshot.input || 0) + deltaInput,
|
|
843
|
+
output: (prevSnapshot.output || 0) + deltaOutput,
|
|
844
|
+
cacheCreation: prevSnapshot.cacheCreation || 0,
|
|
845
|
+
cacheRead: prevSnapshot.cacheRead || 0,
|
|
846
|
+
ts: new Date().toISOString(),
|
|
847
|
+
}));
|
|
848
|
+
} catch { /* best effort */ }
|
|
849
|
+
|
|
850
|
+
// ── Append to costs.jsonl with incrementing entry_id ──────────────────
|
|
851
|
+
const entryId = (session.entry_id || 0) + 1;
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
const costPath = costsJsonlPath(sessionId);
|
|
855
|
+
const record = {
|
|
856
|
+
entry_id: entryId,
|
|
857
|
+
ts: new Date().toISOString(),
|
|
858
|
+
session_id: sessionId,
|
|
859
|
+
model,
|
|
860
|
+
input_tokens: deltaInput,
|
|
861
|
+
output_tokens: deltaOutput,
|
|
862
|
+
cache_creation: 0,
|
|
863
|
+
cache_read: 0,
|
|
864
|
+
total_tokens: deltaTotal,
|
|
865
|
+
cost_usd: Math.round(deltaCost * 1_000_000) / 1_000_000,
|
|
866
|
+
status: 'completed',
|
|
867
|
+
platform: 'cursor',
|
|
868
|
+
};
|
|
869
|
+
appendFileSync(costPath, JSON.stringify(record) + '\n');
|
|
870
|
+
} catch { /* never block */ }
|
|
871
|
+
|
|
872
|
+
// ── Update session metadata ───────────────────────────────────────────
|
|
873
|
+
session.entry_id = entryId;
|
|
874
|
+
session.model = model !== 'unknown' ? model : session.model;
|
|
875
|
+
if (input.platform_version) session.platform_version = input.platform_version;
|
|
876
|
+
|
|
877
|
+
if (transcript.prUrls?.length) {
|
|
878
|
+
const existing = new Set(session.pr_urls || []);
|
|
879
|
+
for (const url of transcript.prUrls) existing.add(url);
|
|
880
|
+
session.pr_urls = [...existing];
|
|
881
|
+
}
|
|
882
|
+
if (transcript.mcpToolsUsed?.length) {
|
|
883
|
+
const existing = new Set(session.mcp_tools || []);
|
|
884
|
+
for (const tool of transcript.mcpToolsUsed) existing.add(tool);
|
|
885
|
+
session.mcp_tools = [...existing];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
writeSession(sessionId, session);
|
|
889
|
+
}
|
|
890
|
+
|
|
722
891
|
/**
|
|
723
892
|
* PreCompact — aggregate unflushed costs.jsonl entries, POST to API.
|
|
724
893
|
* This is the heartbeat for long-running sessions.
|
|
725
894
|
*/
|
|
726
|
-
export async function handlePreCompact(
|
|
727
|
-
const
|
|
895
|
+
export async function handlePreCompact(rawInput) {
|
|
896
|
+
const input = normalizeCursorInput(rawInput);
|
|
897
|
+
const sessionId = input.session_id || 'unknown';
|
|
728
898
|
const session = readSession(sessionId);
|
|
729
899
|
if (!session) return; // no session metadata — nothing to flush
|
|
730
900
|
|
|
@@ -816,8 +986,9 @@ export async function handlePreCompact(input) {
|
|
|
816
986
|
/**
|
|
817
987
|
* SessionEnd — final flush + session duration + cleanup.
|
|
818
988
|
*/
|
|
819
|
-
export async function handleSessionEnd(
|
|
820
|
-
const
|
|
989
|
+
export async function handleSessionEnd(rawInput) {
|
|
990
|
+
const input = normalizeCursorInput(rawInput);
|
|
991
|
+
const sessionId = input.session_id || 'unknown';
|
|
821
992
|
const session = readSession(sessionId);
|
|
822
993
|
if (!session) return;
|
|
823
994
|
|
package/hooks/manifest.mjs
CHANGED
|
@@ -8,10 +8,11 @@ export const CAPABILITIES = {
|
|
|
8
8
|
description: 'Token tracking, cost estimation, API push',
|
|
9
9
|
module: '../capabilities/telemetry.mjs',
|
|
10
10
|
hooks: {
|
|
11
|
-
'session-start':
|
|
12
|
-
'stop':
|
|
13
|
-
'
|
|
14
|
-
'
|
|
11
|
+
'session-start': { handler: 'handleSessionStart', timeout: 5000 },
|
|
12
|
+
'stop': { handler: 'handleStop', timeout: 10000 },
|
|
13
|
+
'after-agent-response': { handler: 'handleAfterAgentResponse', timeout: 10000 },
|
|
14
|
+
'pre-compact': { handler: 'handlePreCompact', timeout: 15000 },
|
|
15
|
+
'session-end': { handler: 'handleSessionEnd', timeout: 15000 },
|
|
15
16
|
},
|
|
16
17
|
},
|
|
17
18
|
'activity-tracker': {
|
package/integrate.mjs
CHANGED
|
@@ -519,6 +519,7 @@ export function installIdeHooks() {
|
|
|
519
519
|
'shared/stop.mjs',
|
|
520
520
|
'shared/pre-compact.mjs',
|
|
521
521
|
'shared/session-end.mjs',
|
|
522
|
+
'shared/after-agent-response.mjs',
|
|
522
523
|
'capabilities/telemetry.mjs',
|
|
523
524
|
];
|
|
524
525
|
|
|
@@ -545,10 +546,11 @@ export function installIdeHooks() {
|
|
|
545
546
|
|
|
546
547
|
// Dispatcher commands — one per hook event
|
|
547
548
|
const dispatchers = {
|
|
548
|
-
SessionStart:
|
|
549
|
-
Stop:
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
SessionStart: { cmd: `node "${join(hooksDir, 'shared', 'session-start.mjs')}"`, timeout: 10 },
|
|
550
|
+
Stop: { cmd: `node "${join(hooksDir, 'shared', 'stop.mjs')}"`, timeout: 15 },
|
|
551
|
+
AfterAgentResponse:{ cmd: `node "${join(hooksDir, 'shared', 'after-agent-response.mjs')}"`, timeout: 15 },
|
|
552
|
+
PreCompact: { cmd: `node "${join(hooksDir, 'shared', 'pre-compact.mjs')}"`, timeout: 20 },
|
|
553
|
+
SessionEnd: { cmd: `node "${join(hooksDir, 'shared', 'session-end.mjs')}"`, timeout: 20 },
|
|
552
554
|
};
|
|
553
555
|
|
|
554
556
|
// ── Step 2: Claude Code — wire 4 events (merge, don't replace) ────
|
|
@@ -562,14 +564,17 @@ export function installIdeHooks() {
|
|
|
562
564
|
}
|
|
563
565
|
if (!claudeSettings.hooks) claudeSettings.hooks = {};
|
|
564
566
|
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
567
|
+
// Claude Code events — skip AfterAgentResponse (Cursor-only)
|
|
568
|
+
const claudeCodeEvents = ['SessionStart', 'Stop', 'PreCompact', 'SessionEnd'];
|
|
569
|
+
const statusMessages = {
|
|
570
|
+
SessionStart: 'Starting telemetry...',
|
|
571
|
+
Stop: 'Recording telemetry...',
|
|
572
|
+
PreCompact: 'Flushing telemetry...',
|
|
573
|
+
SessionEnd: 'Finalizing telemetry...',
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
for (const event of claudeCodeEvents) {
|
|
577
|
+
const { cmd, timeout } = dispatchers[event];
|
|
573
578
|
|
|
574
579
|
// Get existing hooks for this event, preserving non-aw entries
|
|
575
580
|
const existingHooks = claudeSettings.hooks[event]?.[0]?.hooks || [];
|
|
@@ -577,7 +582,6 @@ export function installIdeHooks() {
|
|
|
577
582
|
!h.command?.includes('.aw/hooks/') && !h.command?.includes('telemetry-stop.js')
|
|
578
583
|
);
|
|
579
584
|
|
|
580
|
-
// Build new hook entry
|
|
581
585
|
const awHook = {
|
|
582
586
|
type: 'command',
|
|
583
587
|
command: cmd,
|
|
@@ -618,6 +622,7 @@ export function installIdeHooks() {
|
|
|
618
622
|
const cursorEventMap = {
|
|
619
623
|
SessionStart: 'sessionStart',
|
|
620
624
|
Stop: 'stop',
|
|
625
|
+
AfterAgentResponse: 'afterAgentResponse',
|
|
621
626
|
PreCompact: 'preCompact',
|
|
622
627
|
SessionEnd: 'sessionEnd',
|
|
623
628
|
};
|
|
@@ -697,7 +702,7 @@ export function installIdeHooks() {
|
|
|
697
702
|
} catch { /* best effort */ }
|
|
698
703
|
}
|
|
699
704
|
|
|
700
|
-
fmt.logSuccess('IDE hooks installed —
|
|
705
|
+
fmt.logSuccess('IDE hooks installed — 5 events (Claude + Cursor) + 2 events (Codex) → ~/.aw/hooks/');
|
|
701
706
|
}
|
|
702
707
|
|
|
703
708
|
/**
|