getaimeter 0.3.4 → 0.4.0
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/config.js +8 -0
- package/log-viewer.ps1 +63 -0
- package/package.json +2 -1
- package/tray.ps1 +9 -1
- package/watcher.js +54 -6
package/config.js
CHANGED
|
@@ -68,6 +68,14 @@ function getWatchPaths() {
|
|
|
68
68
|
}
|
|
69
69
|
if (fs.existsSync(desktopSessions)) paths.push(desktopSessions);
|
|
70
70
|
|
|
71
|
+
// 3. OpenAI Codex CLI sessions
|
|
72
|
+
const codexSessions = path.join(os.homedir(), '.codex', 'sessions');
|
|
73
|
+
if (fs.existsSync(codexSessions)) paths.push(codexSessions);
|
|
74
|
+
|
|
75
|
+
// Also watch ~/.codex/history.jsonl directly (Codex writes a single file)
|
|
76
|
+
const codexHistory = path.join(os.homedir(), '.codex');
|
|
77
|
+
if (fs.existsSync(codexHistory) && !paths.includes(codexHistory)) paths.push(codexHistory);
|
|
78
|
+
|
|
71
79
|
return paths;
|
|
72
80
|
}
|
|
73
81
|
|
package/log-viewer.ps1
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$LogPath
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
6
|
+
Add-Type -AssemblyName System.Drawing
|
|
7
|
+
|
|
8
|
+
$form = New-Object System.Windows.Forms.Form
|
|
9
|
+
$form.Text = 'AIMeter — Live Logs'
|
|
10
|
+
$form.Size = New-Object System.Drawing.Size(860, 520)
|
|
11
|
+
$form.BackColor = [System.Drawing.Color]::FromArgb(15, 15, 15)
|
|
12
|
+
$form.StartPosition = 'CenterScreen'
|
|
13
|
+
$form.ShowInTaskbar = $true
|
|
14
|
+
$form.Add_Shown({ $form.Activate() })
|
|
15
|
+
|
|
16
|
+
$rtb = New-Object System.Windows.Forms.RichTextBox
|
|
17
|
+
$rtb.Dock = 'Fill'
|
|
18
|
+
$rtb.BackColor = [System.Drawing.Color]::FromArgb(15, 15, 15)
|
|
19
|
+
$rtb.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 200)
|
|
20
|
+
$rtb.Font = New-Object System.Drawing.Font('Consolas', 9)
|
|
21
|
+
$rtb.ReadOnly = $true
|
|
22
|
+
$rtb.ScrollBars = 'Vertical'
|
|
23
|
+
$rtb.WordWrap = $false
|
|
24
|
+
$form.Controls.Add($rtb)
|
|
25
|
+
|
|
26
|
+
# Seed with last 200 lines
|
|
27
|
+
$script:fileSize = 0
|
|
28
|
+
if (Test-Path $LogPath) {
|
|
29
|
+
$all = Get-Content $LogPath -Raw -ErrorAction SilentlyContinue
|
|
30
|
+
if ($all) {
|
|
31
|
+
$lines = $all -split "`n"
|
|
32
|
+
$seed = ($lines | Select-Object -Last 200) -join "`n"
|
|
33
|
+
$rtb.Text = $seed.TrimEnd() + "`n"
|
|
34
|
+
$rtb.SelectionStart = $rtb.Text.Length
|
|
35
|
+
$rtb.ScrollToCaret()
|
|
36
|
+
}
|
|
37
|
+
$script:fileSize = (Get-Item $LogPath).Length
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Poll for new bytes every 500 ms
|
|
41
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
42
|
+
$timer.Interval = 500
|
|
43
|
+
$timer.Add_Tick({
|
|
44
|
+
if (-not (Test-Path $LogPath)) { return }
|
|
45
|
+
$newSize = (Get-Item $LogPath).Length
|
|
46
|
+
if ($newSize -le $script:fileSize) { return }
|
|
47
|
+
try {
|
|
48
|
+
$fs = [System.IO.File]::Open($LogPath, 'Open', 'Read', 'ReadWrite')
|
|
49
|
+
$null = $fs.Seek($script:fileSize, 'Begin')
|
|
50
|
+
$buf = New-Object byte[] ($newSize - $script:fileSize)
|
|
51
|
+
$null = $fs.Read($buf, 0, $buf.Length)
|
|
52
|
+
$fs.Close()
|
|
53
|
+
$chunk = [System.Text.Encoding]::UTF8.GetString($buf)
|
|
54
|
+
$rtb.AppendText($chunk)
|
|
55
|
+
$rtb.SelectionStart = $rtb.Text.Length
|
|
56
|
+
$rtb.ScrollToCaret()
|
|
57
|
+
$script:fileSize = $newSize
|
|
58
|
+
} catch {}
|
|
59
|
+
})
|
|
60
|
+
$timer.Start()
|
|
61
|
+
|
|
62
|
+
$form.Add_FormClosed({ $timer.Stop(); $timer.Dispose() })
|
|
63
|
+
[System.Windows.Forms.Application]::Run($form)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getaimeter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Track your Claude AI usage across CLI, VS Code, and Desktop App. One command to start.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aimeter": "cli.js"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"service.js",
|
|
32
32
|
"tray.js",
|
|
33
33
|
"tray.ps1",
|
|
34
|
+
"log-viewer.ps1",
|
|
34
35
|
"tray-launcher.vbs",
|
|
35
36
|
"icon.ico",
|
|
36
37
|
"update-check.js",
|
package/tray.ps1
CHANGED
|
@@ -42,7 +42,15 @@ $dashboard = $menu.Items.Add("Open Dashboard")
|
|
|
42
42
|
$dashboard.Add_Click({ Start-Process "https://getaimeter.com/dashboard" })
|
|
43
43
|
|
|
44
44
|
$logs = $menu.Items.Add("View Logs")
|
|
45
|
-
$
|
|
45
|
+
$viewerScript = Join-Path (Split-Path $MyInvocation.MyCommand.Path) 'log-viewer.ps1'
|
|
46
|
+
$logs.Add_Click({
|
|
47
|
+
# Launch log viewer as a separate process so it doesn't block the tray
|
|
48
|
+
# Launch via wscript to avoid any console window flash
|
|
49
|
+
$vbsCmd = "CreateObject(""WScript.Shell"").Run ""powershell -NoProfile -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File """"$viewerScript"""" -LogPath """"$LogPath"""""", 0, False"
|
|
50
|
+
$tempVbs = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'aimeter-logviewer.vbs')
|
|
51
|
+
[System.IO.File]::WriteAllText($tempVbs, $vbsCmd)
|
|
52
|
+
Start-Process 'wscript' -ArgumentList @($tempVbs) -WindowStyle Hidden
|
|
53
|
+
})
|
|
46
54
|
|
|
47
55
|
$menu.Items.Add("-")
|
|
48
56
|
|
package/watcher.js
CHANGED
|
@@ -34,6 +34,9 @@ function logError(...args) {
|
|
|
34
34
|
// Cache detected sources per file to avoid re-reading headers
|
|
35
35
|
const _sourceCache = new Map();
|
|
36
36
|
|
|
37
|
+
// Track cumulative token counts per file for Codex CLI (which reports cumulative, not delta)
|
|
38
|
+
const _codexCumulative = {};
|
|
39
|
+
|
|
37
40
|
function detectSource(filePath) {
|
|
38
41
|
if (_sourceCache.has(filePath)) return _sourceCache.get(filePath);
|
|
39
42
|
|
|
@@ -137,7 +140,50 @@ function extractNewUsage(filePath) {
|
|
|
137
140
|
msgId = msg.id; // progress fires multiple times; dedup by message ID
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
if (!msg)
|
|
143
|
+
if (!msg) {
|
|
144
|
+
// ── Codex CLI format ──────────────────────────────────────────
|
|
145
|
+
// Codex events have { type: "event", payload: { type: "token_count", ... } }
|
|
146
|
+
// with cumulative input_tokens, output_tokens, reasoning_tokens, cached_input_tokens
|
|
147
|
+
if (obj.type === 'event' && obj.payload?.type === 'token_count') {
|
|
148
|
+
const p = obj.payload;
|
|
149
|
+
const codexModel = obj.turn_context?.model || 'codex';
|
|
150
|
+
// Token counts are cumulative per session; we store deltas
|
|
151
|
+
// Use a per-file tracker for the previous cumulative values
|
|
152
|
+
const prevKey = filePath;
|
|
153
|
+
const prev = _codexCumulative[prevKey] || { input: 0, output: 0, reasoning: 0, cached: 0 };
|
|
154
|
+
const deltaInput = (p.input_tokens || 0) - prev.input;
|
|
155
|
+
const deltaOutput = (p.output_tokens || 0) - prev.output;
|
|
156
|
+
const deltaReasoning = (p.reasoning_tokens || 0) - prev.reasoning;
|
|
157
|
+
|
|
158
|
+
_codexCumulative[prevKey] = {
|
|
159
|
+
input: p.input_tokens || 0,
|
|
160
|
+
output: p.output_tokens || 0,
|
|
161
|
+
reasoning: p.reasoning_tokens || 0,
|
|
162
|
+
cached: p.cached_input_tokens || 0,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Skip if no new tokens (duplicate or first read)
|
|
166
|
+
if (deltaInput <= 0 && deltaOutput <= 0) continue;
|
|
167
|
+
|
|
168
|
+
const hashKey = `${filePath}:${lineOffset}:codex:${p.input_tokens}:${p.output_tokens}`;
|
|
169
|
+
const hash = crypto.createHash('md5').update(hashKey).digest('hex');
|
|
170
|
+
if (isDuplicate(hash)) continue;
|
|
171
|
+
|
|
172
|
+
usageEvents.push({
|
|
173
|
+
provider: 'openai',
|
|
174
|
+
model: codexModel,
|
|
175
|
+
source: 'codex_cli',
|
|
176
|
+
inputTokens: deltaInput,
|
|
177
|
+
outputTokens: deltaOutput,
|
|
178
|
+
thinkingTokens: deltaReasoning,
|
|
179
|
+
cacheReadTokens: 0,
|
|
180
|
+
cacheWriteTokens: 0,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
141
187
|
|
|
142
188
|
// Skip synthetic/internal messages
|
|
143
189
|
if (msg.model === '<synthetic>') continue;
|
|
@@ -169,11 +215,13 @@ function extractNewUsage(filePath) {
|
|
|
169
215
|
pendingThinkingChars = 0; // Reset for next turn
|
|
170
216
|
|
|
171
217
|
// Build dedup hash.
|
|
172
|
-
// For
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
// For entries with a message ID (progress or assistant), use ONLY the message
|
|
219
|
+
// ID so the same call is counted once even if it appears in both the parent
|
|
220
|
+
// session file (as progress) and the subagent file (as assistant).
|
|
221
|
+
// For entries without an ID, fall back to file+offset.
|
|
222
|
+
const effectiveId = msgId || msg.id;
|
|
223
|
+
const hashKey = effectiveId
|
|
224
|
+
? `msgid:${effectiveId}`
|
|
177
225
|
: `${filePath}:${lineOffset}:${model}:${u.input_tokens || 0}:${u.output_tokens || 0}`;
|
|
178
226
|
const hash = crypto.createHash('md5').update(hashKey).digest('hex');
|
|
179
227
|
|