@ghl-ai/aw 0.1.37-beta.70 → 0.1.37-beta.72
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/integrate.mjs +13 -74
- package/package.json +1 -1
- package/hooks/activity-tracker.js +0 -61
- package/hooks/telemetry-stop.js +0 -411
package/integrate.mjs
CHANGED
|
@@ -522,27 +522,10 @@ export function installIdeHooks() {
|
|
|
522
522
|
'capabilities/telemetry.mjs',
|
|
523
523
|
];
|
|
524
524
|
|
|
525
|
-
//
|
|
526
|
-
const legacyFiles = ['telemetry-stop.js', 'activity-tracker.js'];
|
|
527
|
-
|
|
528
|
-
// Verify new hook files exist in the package
|
|
525
|
+
// Verify hook files exist in the package
|
|
529
526
|
const hasNewHooks = newHookFiles.every(f => existsSync(join(pkgHooksDir, f)));
|
|
530
527
|
if (!hasNewHooks) {
|
|
531
|
-
|
|
532
|
-
const hasLegacy = legacyFiles.every(f => existsSync(join(pkgHooksDir, f)));
|
|
533
|
-
if (!hasLegacy) {
|
|
534
|
-
fmt.logWarn('Hook scripts not found in aw package — skipping IDE hooks');
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
// Install legacy hooks only
|
|
538
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
539
|
-
for (const file of legacyFiles) {
|
|
540
|
-
const dest = join(hooksDir, file);
|
|
541
|
-
writeFileSync(dest, readFileSync(join(pkgHooksDir, file), 'utf8'));
|
|
542
|
-
try { chmodSync(dest, 0o755); } catch { /* best effort */ }
|
|
543
|
-
}
|
|
544
|
-
_wireLegacyHooks(home, hooksDir);
|
|
545
|
-
fmt.logSuccess('Legacy IDE hooks installed (Claude + Cursor → ~/.aw/hooks/)');
|
|
528
|
+
fmt.logWarn('Hook scripts not found in aw package — skipping IDE hooks');
|
|
546
529
|
return;
|
|
547
530
|
}
|
|
548
531
|
|
|
@@ -560,16 +543,6 @@ export function installIdeHooks() {
|
|
|
560
543
|
try { chmodSync(dest, 0o755); } catch { /* best effort */ }
|
|
561
544
|
}
|
|
562
545
|
|
|
563
|
-
// Also copy legacy files for rollback
|
|
564
|
-
for (const file of legacyFiles) {
|
|
565
|
-
const src = join(pkgHooksDir, file);
|
|
566
|
-
if (existsSync(src)) {
|
|
567
|
-
const dest = join(hooksDir, file);
|
|
568
|
-
writeFileSync(dest, readFileSync(src, 'utf8'));
|
|
569
|
-
try { chmodSync(dest, 0o755); } catch { /* best effort */ }
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
546
|
// Dispatcher commands — one per hook event
|
|
574
547
|
const dispatchers = {
|
|
575
548
|
SessionStart: { cmd: `node "${join(hooksDir, 'shared', 'session-start.mjs')}"`, timeout: 10 },
|
|
@@ -680,28 +653,24 @@ export function installIdeHooks() {
|
|
|
680
653
|
mkdirSync(codexDir, { recursive: true });
|
|
681
654
|
const codexHooksPath = join(codexDir, 'hooks.json');
|
|
682
655
|
|
|
683
|
-
let
|
|
656
|
+
let codexHooksFile = {};
|
|
684
657
|
if (existsSync(codexHooksPath)) {
|
|
685
|
-
try {
|
|
658
|
+
try { codexHooksFile = JSON.parse(readFileSync(codexHooksPath, 'utf8')); } catch { codexHooksFile = {}; }
|
|
686
659
|
}
|
|
660
|
+
if (!codexHooksFile.hooks) codexHooksFile.hooks = {};
|
|
687
661
|
|
|
688
662
|
// Codex supports: SessionStart, Stop, PreToolUse, PostToolUse, UserPromptSubmit
|
|
689
663
|
const codexEvents = { SessionStart: dispatchers.SessionStart, Stop: dispatchers.Stop };
|
|
690
664
|
for (const [event, { cmd, timeout }] of Object.entries(codexEvents)) {
|
|
691
|
-
if (!
|
|
665
|
+
if (!codexHooksFile.hooks[event]) codexHooksFile.hooks[event] = [];
|
|
692
666
|
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
g.hooks?.some(h => h.command?.includes('.aw/hooks/'))
|
|
667
|
+
// Remove old aw entries
|
|
668
|
+
codexHooksFile.hooks[event] = codexHooksFile.hooks[event].filter(h =>
|
|
669
|
+
!h.command?.includes('.aw/hooks/')
|
|
697
670
|
);
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
// Replace aw hooks in the group
|
|
703
|
-
awGroup.hooks = awGroup.hooks.filter(h => !h.command?.includes('.aw/hooks/'));
|
|
704
|
-
awGroup.hooks.push({
|
|
671
|
+
|
|
672
|
+
// Codex hooks are flat objects: { command, type, timeoutSec, statusMessage }
|
|
673
|
+
codexHooksFile.hooks[event].push({
|
|
705
674
|
type: 'command',
|
|
706
675
|
command: cmd,
|
|
707
676
|
timeoutSec: timeout,
|
|
@@ -709,7 +678,7 @@ export function installIdeHooks() {
|
|
|
709
678
|
});
|
|
710
679
|
}
|
|
711
680
|
|
|
712
|
-
writeFileSync(codexHooksPath, JSON.stringify(
|
|
681
|
+
writeFileSync(codexHooksPath, JSON.stringify(codexHooksFile, null, 2) + '\n');
|
|
713
682
|
|
|
714
683
|
// Enable codex_hooks feature if not already set
|
|
715
684
|
const codexConfigPath = join(codexDir, 'config.toml');
|
|
@@ -731,36 +700,6 @@ export function installIdeHooks() {
|
|
|
731
700
|
fmt.logSuccess('IDE hooks installed — 4 events (Claude + Cursor) + 2 events (Codex) → ~/.aw/hooks/');
|
|
732
701
|
}
|
|
733
702
|
|
|
734
|
-
/** Legacy hook wiring — used as fallback when new dispatchers aren't available */
|
|
735
|
-
function _wireLegacyHooks(home, hooksDir) {
|
|
736
|
-
const stopCmd = `node "${join(hooksDir, 'telemetry-stop.js')}"`;
|
|
737
|
-
const activityCmd = `node "${join(hooksDir, 'activity-tracker.js')}"`;
|
|
738
|
-
|
|
739
|
-
// Claude Code
|
|
740
|
-
const claudeSettingsPath = join(home, '.claude', 'settings.json');
|
|
741
|
-
let claudeSettings = {};
|
|
742
|
-
if (existsSync(claudeSettingsPath)) {
|
|
743
|
-
try { claudeSettings = JSON.parse(readFileSync(claudeSettingsPath, 'utf8')); } catch { claudeSettings = {}; }
|
|
744
|
-
}
|
|
745
|
-
if (!claudeSettings.hooks) claudeSettings.hooks = {};
|
|
746
|
-
claudeSettings.hooks.Stop = [{ hooks: [{ type: 'command', command: stopCmd, timeout: 15, statusMessage: 'Recording telemetry...' }] }];
|
|
747
|
-
claudeSettings.hooks.PostToolUse = [{ hooks: [{ type: 'command', command: activityCmd, timeout: 5 }] }];
|
|
748
|
-
writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n');
|
|
749
|
-
|
|
750
|
-
// Cursor
|
|
751
|
-
const cursorHooksPath = join(home, '.cursor', 'hooks.json');
|
|
752
|
-
let cursorHooks = { version: 1, hooks: {} };
|
|
753
|
-
if (existsSync(cursorHooksPath)) {
|
|
754
|
-
try { cursorHooks = JSON.parse(readFileSync(cursorHooksPath, 'utf8')); } catch { /* overwrite */ }
|
|
755
|
-
}
|
|
756
|
-
if (!cursorHooks.hooks) cursorHooks.hooks = {};
|
|
757
|
-
if (!cursorHooks.hooks.stop) cursorHooks.hooks.stop = [];
|
|
758
|
-
if (!cursorHooks.hooks.stop.some(h => h.command?.includes('telemetry-stop.js'))) {
|
|
759
|
-
cursorHooks.hooks.stop.push({ command: stopCmd, timeout: 15 });
|
|
760
|
-
}
|
|
761
|
-
writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n');
|
|
762
|
-
}
|
|
763
|
-
|
|
764
703
|
/**
|
|
765
704
|
* Return top-level team namespace names from config (excludes 'platform').
|
|
766
705
|
* cfg.include may contain full paths like 'mobile/core/backend/agents/dev.md'
|
package/package.json
CHANGED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// activity-tracker.js — Lightweight PostToolUse hook for Claude Code + Cursor.
|
|
4
|
-
//
|
|
5
|
-
// Fires after every tool call. Appends one line to .claude/telemetry/activity.jsonl.
|
|
6
|
-
// MUST complete in <100ms — no child processes, no network, pure fs append.
|
|
7
|
-
// MUST never exit non-zero or block the agent loop.
|
|
8
|
-
|
|
9
|
-
const { appendFileSync, mkdirSync } = require('node:fs');
|
|
10
|
-
const { join } = require('node:path');
|
|
11
|
-
|
|
12
|
-
async function main() {
|
|
13
|
-
// Read stdin (JSON from hook system)
|
|
14
|
-
const chunks = [];
|
|
15
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
16
|
-
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
17
|
-
if (!raw) return;
|
|
18
|
-
|
|
19
|
-
let input;
|
|
20
|
-
try { input = JSON.parse(raw); } catch { return; }
|
|
21
|
-
|
|
22
|
-
// Normalize platform differences
|
|
23
|
-
// Claude Code: { tool_name, tool_input, tool_response, cwd, session_id }
|
|
24
|
-
// Cursor: { tool_name, tool_input, tool_output, cwd, duration, conversation_id }
|
|
25
|
-
const toolName = input.tool_name || 'unknown';
|
|
26
|
-
const sessionId = input.session_id || input.conversation_id || 'unknown';
|
|
27
|
-
const cwd = input.cwd
|
|
28
|
-
|| process.env.CURSOR_PROJECT_DIR
|
|
29
|
-
|| process.env.CLAUDE_PROJECT_DIR
|
|
30
|
-
|| process.cwd();
|
|
31
|
-
const duration = input.duration || 0;
|
|
32
|
-
|
|
33
|
-
// Extract exit code (Claude Code nests it in tool_response)
|
|
34
|
-
let exitCode = 0;
|
|
35
|
-
const resp = input.tool_response || input.tool_output;
|
|
36
|
-
if (resp && typeof resp === 'object') {
|
|
37
|
-
exitCode = resp.exitCode ?? 0;
|
|
38
|
-
} else if (typeof resp === 'string') {
|
|
39
|
-
try {
|
|
40
|
-
const parsed = JSON.parse(resp);
|
|
41
|
-
exitCode = parsed.exitCode ?? 0;
|
|
42
|
-
} catch { /* not JSON */ }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Append to activity.jsonl (atomic for writes < PIPE_BUF = 4096 bytes)
|
|
46
|
-
const telemetryDir = join(cwd, '.claude', 'telemetry');
|
|
47
|
-
try {
|
|
48
|
-
mkdirSync(telemetryDir, { recursive: true });
|
|
49
|
-
const record = JSON.stringify({
|
|
50
|
-
ts: new Date().toISOString(),
|
|
51
|
-
sid: sessionId,
|
|
52
|
-
tool: toolName,
|
|
53
|
-
exit: exitCode,
|
|
54
|
-
dur: duration,
|
|
55
|
-
});
|
|
56
|
-
// Single appendFileSync — atomic on most OS for small writes
|
|
57
|
-
appendFileSync(join(telemetryDir, 'activity.jsonl'), record + '\n');
|
|
58
|
-
} catch { /* never block on write failure */ }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
main().catch(() => process.exit(0));
|
package/hooks/telemetry-stop.js
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// telemetry-stop.js — Unified Stop hook for Claude Code + Cursor.
|
|
4
|
-
//
|
|
5
|
-
// Fires when the AI stops responding. Reads the session transcript,
|
|
6
|
-
// extracts token counts + model, estimates cost, and:
|
|
7
|
-
// 1. Appends to .claude/telemetry/costs.jsonl (local, always works)
|
|
8
|
-
// 2. Pushes unpushed perf-summary.json files via `aw telemetry push`
|
|
9
|
-
//
|
|
10
|
-
// Works on BOTH platforms — normalizes the minor stdin schema differences.
|
|
11
|
-
// MUST never exit non-zero or block the agent loop.
|
|
12
|
-
//
|
|
13
|
-
// Installed to ~/.aw/hooks/ by `aw init`. Referenced by:
|
|
14
|
-
// ~/.claude/settings.json (Stop hook)
|
|
15
|
-
// ~/.cursor/hooks.json (stop hook)
|
|
16
|
-
|
|
17
|
-
const { readFileSync, appendFileSync, mkdirSync, existsSync } = require('node:fs');
|
|
18
|
-
const { join } = require('node:path');
|
|
19
|
-
|
|
20
|
-
// ── API URL resolution ─────────────────────────────────────────────────
|
|
21
|
-
// Read from: 1) env var 2) ~/.aw/config.json 3) hardcoded staging
|
|
22
|
-
function resolveApiUrl() {
|
|
23
|
-
if (process.env.AW_API_URL) return `${process.env.AW_API_URL}/telemetry/ingest`;
|
|
24
|
-
// Read from config written by `aw init`
|
|
25
|
-
const configPath = join(require('node:os').homedir(), '.aw', 'config.json');
|
|
26
|
-
if (existsSync(configPath)) {
|
|
27
|
-
try {
|
|
28
|
-
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
29
|
-
if (cfg.api_url) return `${cfg.api_url}/telemetry/ingest`;
|
|
30
|
-
} catch { /* corrupted */ }
|
|
31
|
-
}
|
|
32
|
-
return 'https://staging.services.leadconnectorhq.com/agentic-workspace/telemetry/ingest';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ── Direct HTTP push (no shelling out to aw CLI) ───────────────────────
|
|
36
|
-
async function pushToApi(payload) {
|
|
37
|
-
const url = resolveApiUrl();
|
|
38
|
-
const controller = new AbortController();
|
|
39
|
-
const timer = setTimeout(() => controller.abort(), 8_000);
|
|
40
|
-
try {
|
|
41
|
-
const res = await fetch(url, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
body: JSON.stringify(payload),
|
|
45
|
-
signal: controller.signal,
|
|
46
|
-
});
|
|
47
|
-
return res.ok;
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
} finally {
|
|
51
|
-
clearTimeout(timer);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── Resolve github login for attribution ───────────────────────────────
|
|
56
|
-
function resolveGithubLogin() {
|
|
57
|
-
// Try .aw_registry/.token first
|
|
58
|
-
const tokenPath = join(require('node:os').homedir(), '.aw_registry', '.token');
|
|
59
|
-
if (existsSync(tokenPath)) {
|
|
60
|
-
try {
|
|
61
|
-
const t = JSON.parse(readFileSync(tokenPath, 'utf8'));
|
|
62
|
-
if (t.github_login) return t.github_login;
|
|
63
|
-
} catch { /* corrupted */ }
|
|
64
|
-
}
|
|
65
|
-
// Fallback to git email
|
|
66
|
-
try {
|
|
67
|
-
return require('node:child_process')
|
|
68
|
-
.execSync('git config --global user.email', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] })
|
|
69
|
-
.trim() || null;
|
|
70
|
-
} catch { return null; }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Token pricing (per 1M tokens) ──────────────────────────────────────
|
|
74
|
-
const PRICING = {
|
|
75
|
-
// Anthropic Claude
|
|
76
|
-
'claude-haiku': { input: 0.80, output: 4.00 },
|
|
77
|
-
'claude-sonnet': { input: 3.00, output: 15.00 },
|
|
78
|
-
'claude-opus': { input: 15.00, output: 75.00 },
|
|
79
|
-
'haiku': { input: 0.80, output: 4.00 },
|
|
80
|
-
'sonnet': { input: 3.00, output: 15.00 },
|
|
81
|
-
'opus': { input: 15.00, output: 75.00 },
|
|
82
|
-
// OpenAI GPT
|
|
83
|
-
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
84
|
-
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
85
|
-
'gpt-4-turbo': { input: 10.00, output: 30.00 },
|
|
86
|
-
'gpt-4': { input: 30.00, output: 60.00 },
|
|
87
|
-
'gpt-3.5': { input: 0.50, output: 1.50 },
|
|
88
|
-
'o1': { input: 15.00, output: 60.00 },
|
|
89
|
-
'o1-mini': { input: 3.00, output: 12.00 },
|
|
90
|
-
'o1-pro': { input: 150.00, output: 600.00 },
|
|
91
|
-
'o3': { input: 10.00, output: 40.00 },
|
|
92
|
-
'o3-mini': { input: 1.10, output: 4.40 },
|
|
93
|
-
'o4-mini': { input: 1.10, output: 4.40 },
|
|
94
|
-
// Google Gemini
|
|
95
|
-
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
96
|
-
'gemini-2.5-flash': { input: 0.15, output: 0.60 },
|
|
97
|
-
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
98
|
-
'gemini-1.5-pro': { input: 1.25, output: 5.00 },
|
|
99
|
-
'gemini-1.5-flash': { input: 0.075, output: 0.30 },
|
|
100
|
-
// Cursor Composer (uses Claude/GPT under the hood — estimate)
|
|
101
|
-
'composer': { input: 3.00, output: 15.00 },
|
|
102
|
-
'composer-2': { input: 3.00, output: 15.00 },
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
function estimateCost(model, inputTokens, outputTokens) {
|
|
106
|
-
const key = Object.keys(PRICING).find(k => (model || '').toLowerCase().includes(k));
|
|
107
|
-
const rates = key ? PRICING[key] : PRICING['sonnet'];
|
|
108
|
-
return (inputTokens / 1_000_000) * rates.input + (outputTokens / 1_000_000) * rates.output;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ── Parse stdin ────────────────────────────────────────────────────────
|
|
112
|
-
async function readStdin() {
|
|
113
|
-
const chunks = [];
|
|
114
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
115
|
-
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
116
|
-
if (!raw) return {};
|
|
117
|
-
try { return JSON.parse(raw); } catch { return {}; }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ── Extract tokens from transcript JSONL ────────────────────────────────
|
|
121
|
-
// Claude Code usage entries are cumulative per conversation.
|
|
122
|
-
// We take the LAST usage entry (final token count for the session).
|
|
123
|
-
function extractFromTranscript(transcriptPath) {
|
|
124
|
-
if (!transcriptPath || !existsSync(transcriptPath)) return null;
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const content = readFileSync(transcriptPath, 'utf8');
|
|
128
|
-
const lines = content.split('\n').filter(Boolean);
|
|
129
|
-
|
|
130
|
-
let lastInputTokens = 0;
|
|
131
|
-
let lastOutputTokens = 0;
|
|
132
|
-
let lastCacheCreation = 0;
|
|
133
|
-
let lastCacheRead = 0;
|
|
134
|
-
let model = null;
|
|
135
|
-
let command = null;
|
|
136
|
-
const agentsUsed = new Set();
|
|
137
|
-
const skillsApplied = new Set();
|
|
138
|
-
let toolTotal = 0;
|
|
139
|
-
let toolFailed = 0;
|
|
140
|
-
|
|
141
|
-
for (const line of lines) {
|
|
142
|
-
try {
|
|
143
|
-
const entry = JSON.parse(line);
|
|
144
|
-
if (entry.model) model = entry.model;
|
|
145
|
-
|
|
146
|
-
// Token usage — Claude Code format (cumulative)
|
|
147
|
-
const usage = entry.usage || entry.message?.usage;
|
|
148
|
-
if (usage) {
|
|
149
|
-
lastInputTokens = usage.input_tokens || 0;
|
|
150
|
-
lastOutputTokens = usage.output_tokens || 0;
|
|
151
|
-
lastCacheCreation = usage.cache_creation_input_tokens || 0;
|
|
152
|
-
lastCacheRead = usage.cache_read_input_tokens || 0;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Claude Code nests tool calls inside assistant message.content[]
|
|
156
|
-
const contentBlocks = entry.message?.content;
|
|
157
|
-
if (Array.isArray(contentBlocks)) {
|
|
158
|
-
for (const block of contentBlocks) {
|
|
159
|
-
if (!block || block.type !== 'tool_use') continue;
|
|
160
|
-
const name = block.name || '';
|
|
161
|
-
const inp = block.input || {};
|
|
162
|
-
|
|
163
|
-
if (name === 'Skill' && inp.skill) {
|
|
164
|
-
command = String(inp.skill).slice(0, 128);
|
|
165
|
-
skillsApplied.add(command);
|
|
166
|
-
}
|
|
167
|
-
if (name === 'Agent' && inp.subagent_type) {
|
|
168
|
-
agentsUsed.add(String(inp.subagent_type).slice(0, 128));
|
|
169
|
-
}
|
|
170
|
-
if (name && name !== 'Skill') {
|
|
171
|
-
toolTotal++;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Also handle flat tool_name format (Cursor, older Claude)
|
|
177
|
-
const toolName = entry.tool_name;
|
|
178
|
-
const toolInput = entry.tool_input || {};
|
|
179
|
-
if (toolName === 'Skill' && toolInput.skill) {
|
|
180
|
-
command = String(toolInput.skill).slice(0, 128);
|
|
181
|
-
skillsApplied.add(command);
|
|
182
|
-
}
|
|
183
|
-
if (toolName === 'Agent' && toolInput.subagent_type) {
|
|
184
|
-
agentsUsed.add(String(toolInput.subagent_type).slice(0, 128));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Count tool failures from tool results
|
|
188
|
-
const result = entry.toolUseResult;
|
|
189
|
-
if (result && typeof result === 'object') {
|
|
190
|
-
const ec = result.exitCode ?? result.exit_code;
|
|
191
|
-
if (ec && ec !== 0) toolFailed++;
|
|
192
|
-
}
|
|
193
|
-
} catch { /* skip malformed lines */ }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Cursor fallback: estimate tokens from content length (~4 chars/token)
|
|
197
|
-
// when no usage data is available (Cursor doesn't write usage entries)
|
|
198
|
-
if (lastInputTokens === 0 && lastOutputTokens === 0) {
|
|
199
|
-
let inputChars = 0;
|
|
200
|
-
let outputChars = 0;
|
|
201
|
-
for (const line of lines) {
|
|
202
|
-
try {
|
|
203
|
-
const entry = JSON.parse(line);
|
|
204
|
-
const msgContent = entry.message?.content;
|
|
205
|
-
const len = Array.isArray(msgContent)
|
|
206
|
-
? msgContent.reduce((s, c) => s + (typeof c === 'string' ? c.length : JSON.stringify(c).length), 0)
|
|
207
|
-
: typeof msgContent === 'string' ? msgContent.length : 0;
|
|
208
|
-
if (entry.role === 'user') inputChars += len;
|
|
209
|
-
else if (entry.role === 'assistant') outputChars += len;
|
|
210
|
-
} catch {}
|
|
211
|
-
}
|
|
212
|
-
if (inputChars + outputChars > 0) {
|
|
213
|
-
lastInputTokens = Math.round(inputChars / 4);
|
|
214
|
-
lastOutputTokens = Math.round(outputChars / 4);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
inputTokens: lastInputTokens,
|
|
220
|
-
outputTokens: lastOutputTokens,
|
|
221
|
-
cacheCreation: lastCacheCreation,
|
|
222
|
-
cacheRead: lastCacheRead,
|
|
223
|
-
totalTokens: lastInputTokens + lastOutputTokens + lastCacheCreation + lastCacheRead,
|
|
224
|
-
model,
|
|
225
|
-
command,
|
|
226
|
-
agentsUsed: [...agentsUsed],
|
|
227
|
-
skillsApplied: [...skillsApplied],
|
|
228
|
-
toolTotal,
|
|
229
|
-
toolPassed: toolTotal - toolFailed,
|
|
230
|
-
toolFailed,
|
|
231
|
-
};
|
|
232
|
-
} catch {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Main ────────────────────────────────────────────────────────────────
|
|
238
|
-
async function main() {
|
|
239
|
-
const input = await readStdin();
|
|
240
|
-
|
|
241
|
-
const sessionId = input.session_id || input.conversation_id || 'unknown';
|
|
242
|
-
const transcriptPath = input.transcript_path
|
|
243
|
-
|| process.env.CURSOR_TRANSCRIPT_PATH
|
|
244
|
-
|| null;
|
|
245
|
-
const cwd = input.cwd
|
|
246
|
-
|| process.env.CURSOR_PROJECT_DIR
|
|
247
|
-
|| process.env.CLAUDE_PROJECT_DIR
|
|
248
|
-
|| process.cwd();
|
|
249
|
-
const status = input.status || 'completed';
|
|
250
|
-
const stdinModel = input.model || null;
|
|
251
|
-
|
|
252
|
-
const transcript = extractFromTranscript(transcriptPath);
|
|
253
|
-
const model = stdinModel || transcript?.model || 'unknown';
|
|
254
|
-
|
|
255
|
-
// ── Token delta: diff current cumulative vs previous snapshot ────────
|
|
256
|
-
// Claude Code reports cumulative session totals. By snapshotting after
|
|
257
|
-
// each Stop event, we get the per-response delta.
|
|
258
|
-
const { writeFileSync } = require('node:fs');
|
|
259
|
-
const telemetryDir = join(cwd, '.claude', 'telemetry');
|
|
260
|
-
mkdirSync(telemetryDir, { recursive: true });
|
|
261
|
-
|
|
262
|
-
const snapshotPath = join(telemetryDir, `.token-snapshot-${sessionId}`);
|
|
263
|
-
let prevSnapshot = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
264
|
-
if (existsSync(snapshotPath)) {
|
|
265
|
-
try { prevSnapshot = JSON.parse(readFileSync(snapshotPath, 'utf8')); } catch { /* corrupted */ }
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const currInput = transcript?.inputTokens || 0;
|
|
269
|
-
const currOutput = transcript?.outputTokens || 0;
|
|
270
|
-
const currCacheCreation = transcript?.cacheCreation || 0;
|
|
271
|
-
const currCacheRead = transcript?.cacheRead || 0;
|
|
272
|
-
|
|
273
|
-
// Delta = current cumulative - previous snapshot
|
|
274
|
-
const deltaInput = Math.max(0, currInput - (prevSnapshot.input || 0));
|
|
275
|
-
const deltaOutput = Math.max(0, currOutput - (prevSnapshot.output || 0));
|
|
276
|
-
const deltaCacheCreation = Math.max(0, currCacheCreation - (prevSnapshot.cacheCreation || 0));
|
|
277
|
-
const deltaCacheRead = Math.max(0, currCacheRead - (prevSnapshot.cacheRead || 0));
|
|
278
|
-
const deltaTotal = deltaInput + deltaOutput + deltaCacheCreation + deltaCacheRead;
|
|
279
|
-
const deltaCost = estimateCost(model, deltaInput, deltaOutput);
|
|
280
|
-
|
|
281
|
-
// Save current cumulative as snapshot for next time
|
|
282
|
-
try {
|
|
283
|
-
writeFileSync(snapshotPath, JSON.stringify({
|
|
284
|
-
input: currInput, output: currOutput,
|
|
285
|
-
cacheCreation: currCacheCreation, cacheRead: currCacheRead,
|
|
286
|
-
ts: new Date().toISOString(),
|
|
287
|
-
}));
|
|
288
|
-
} catch { /* best effort */ }
|
|
289
|
-
|
|
290
|
-
// ── 1. Local JSONL (always works, even offline) ─────────────────────
|
|
291
|
-
try {
|
|
292
|
-
const record = {
|
|
293
|
-
ts: new Date().toISOString(),
|
|
294
|
-
session_id: sessionId,
|
|
295
|
-
model,
|
|
296
|
-
input_tokens: deltaInput,
|
|
297
|
-
output_tokens: deltaOutput,
|
|
298
|
-
cache_creation: deltaCacheCreation,
|
|
299
|
-
cache_read: deltaCacheRead,
|
|
300
|
-
total_tokens: deltaTotal,
|
|
301
|
-
cost_usd: Math.round(deltaCost * 1_000_000) / 1_000_000,
|
|
302
|
-
status,
|
|
303
|
-
platform: input.cursor_version ? 'cursor' : 'claude',
|
|
304
|
-
};
|
|
305
|
-
appendFileSync(join(telemetryDir, 'costs.jsonl'), JSON.stringify(record) + '\n');
|
|
306
|
-
} catch { /* never block on local write failure */ }
|
|
307
|
-
|
|
308
|
-
// ── 2. Push unpushed perf-summary.json from .aw_docs/runs/ ──────────
|
|
309
|
-
// Direct HTTP POST — no shelling out to aw CLI (avoids env/path issues).
|
|
310
|
-
try {
|
|
311
|
-
const { readdirSync } = require('node:fs');
|
|
312
|
-
const { homedir } = require('node:os');
|
|
313
|
-
const searchPaths = [...new Set([
|
|
314
|
-
join(cwd, '.aw_docs', 'runs'),
|
|
315
|
-
join(homedir(), '.aw_docs', 'runs'),
|
|
316
|
-
])];
|
|
317
|
-
|
|
318
|
-
for (const runsDir of searchPaths) {
|
|
319
|
-
if (!existsSync(runsDir)) continue;
|
|
320
|
-
const runDirs = readdirSync(runsDir, { withFileTypes: true })
|
|
321
|
-
.filter(d => d.isDirectory())
|
|
322
|
-
.map(d => join(runsDir, d.name));
|
|
323
|
-
|
|
324
|
-
for (const runDir of runDirs) {
|
|
325
|
-
const perfPath = join(runDir, 'perf-summary.json');
|
|
326
|
-
const pushedPath = join(runDir, '.pushed');
|
|
327
|
-
if (!existsSync(perfPath) || existsSync(pushedPath)) continue;
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
const perf = JSON.parse(readFileSync(perfPath, 'utf8'));
|
|
331
|
-
|
|
332
|
-
// Enrich: hook-derived data fills gaps left by the command
|
|
333
|
-
if ((!perf.tokens_used || perf.tokens_used === null) && deltaTotal > 0) {
|
|
334
|
-
perf.tokens_used = deltaTotal;
|
|
335
|
-
}
|
|
336
|
-
if ((!perf.cost_usd || perf.cost_usd === null) && deltaCost > 0) {
|
|
337
|
-
perf.cost_usd = Math.round(deltaCost * 1_000_000) / 1_000_000;
|
|
338
|
-
}
|
|
339
|
-
if (!perf.model && model !== 'unknown') {
|
|
340
|
-
perf.model = model;
|
|
341
|
-
}
|
|
342
|
-
if (!perf.environment) {
|
|
343
|
-
perf.environment = input.cursor_version ? 'cursor' : 'claude-code';
|
|
344
|
-
}
|
|
345
|
-
// Enrich agents/skills from transcript if command left them empty
|
|
346
|
-
if ((!perf.agents_used || perf.agents_used.length === 0) && transcript?.agentsUsed?.length) {
|
|
347
|
-
perf.agents_used = transcript.agentsUsed;
|
|
348
|
-
}
|
|
349
|
-
if ((!perf.skills_applied || perf.skills_applied.length === 0) && transcript?.skillsApplied?.length) {
|
|
350
|
-
perf.skills_applied = transcript.skillsApplied;
|
|
351
|
-
}
|
|
352
|
-
// Enrich steps from tool call counts if command left them null/zero
|
|
353
|
-
if (!perf.steps_total && transcript?.toolTotal) {
|
|
354
|
-
perf.steps_total = transcript.toolTotal;
|
|
355
|
-
perf.steps_passed = transcript.toolPassed;
|
|
356
|
-
perf.steps_failed = transcript.toolFailed;
|
|
357
|
-
}
|
|
358
|
-
// Enrich branch if missing
|
|
359
|
-
if (!perf.branch || perf.branch === 'no-branch') {
|
|
360
|
-
try {
|
|
361
|
-
perf.branch = require('node:child_process')
|
|
362
|
-
.execSync('git branch --show-current 2>/dev/null', { encoding: 'utf8', cwd })
|
|
363
|
-
.trim() || null;
|
|
364
|
-
} catch { /* not a git repo */ }
|
|
365
|
-
}
|
|
366
|
-
writeFileSync(perfPath, JSON.stringify(perf, null, 2) + '\n');
|
|
367
|
-
|
|
368
|
-
// Build DTO matching IngestRunDto and POST directly
|
|
369
|
-
const dto = {
|
|
370
|
-
shell_run_id: perf.run_id,
|
|
371
|
-
command: perf.command,
|
|
372
|
-
status: perf.status || 'complete',
|
|
373
|
-
branch: perf.branch || null,
|
|
374
|
-
environment: perf.environment || null,
|
|
375
|
-
model: perf.model || null,
|
|
376
|
-
duration_ms: perf.duration_ms || null,
|
|
377
|
-
tokens_used: perf.tokens_used || null,
|
|
378
|
-
cost_usd: perf.cost_usd || null,
|
|
379
|
-
first_pass_rate: perf.first_pass_rate || null,
|
|
380
|
-
steps_total: perf.steps_total || null,
|
|
381
|
-
steps_passed: perf.steps_passed || null,
|
|
382
|
-
steps_failed: perf.steps_failed || null,
|
|
383
|
-
steps_skipped: perf.steps_skipped || null,
|
|
384
|
-
retries_total: perf.retries_total || null,
|
|
385
|
-
knowledge_used_count: perf.knowledge_used_count || null,
|
|
386
|
-
knowledge_saved_count: perf.knowledge_saved_count || null,
|
|
387
|
-
agents_used: perf.agents_used || [],
|
|
388
|
-
skills_applied: perf.skills_applied || [],
|
|
389
|
-
github_login: resolveGithubLogin(),
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
if (dto.shell_run_id && dto.command) {
|
|
393
|
-
const pushed = await pushToApi(dto);
|
|
394
|
-
if (pushed) {
|
|
395
|
-
// Clean up: remove perf-summary + run dir after successful push
|
|
396
|
-
const { rmSync } = require('node:fs');
|
|
397
|
-
try { rmSync(runDir, { recursive: true, force: true }); } catch {}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
} catch { /* best effort */ }
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
} catch { /* never block */ }
|
|
404
|
-
|
|
405
|
-
process.stdout.write('{}\n');
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
main().catch(() => {
|
|
409
|
-
process.stdout.write('{}\n');
|
|
410
|
-
process.exit(0);
|
|
411
|
-
});
|