getaimeter 0.10.0 → 0.11.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/cli.js +657 -657
- package/package.json +1 -1
- package/tray.ps1 +208 -208
- package/watcher.js +36 -8
package/package.json
CHANGED
package/tray.ps1
CHANGED
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
param(
|
|
2
|
-
[string]$IconPath,
|
|
3
|
-
[string]$MaskedKey,
|
|
4
|
-
[string]$LogPath,
|
|
5
|
-
[int]$ParentPid,
|
|
6
|
-
[string]$Version = "unknown"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
10
|
-
Add-Type -AssemblyName System.Drawing
|
|
11
|
-
|
|
12
|
-
# ── Prevent duplicate tray instances ──────────────────────────────────────────
|
|
13
|
-
$mutexName = "Global\AIMeterTray"
|
|
14
|
-
$createdNew = $false
|
|
15
|
-
$mutex = New-Object System.Threading.Mutex($true, $mutexName, [ref]$createdNew)
|
|
16
|
-
if (-not $createdNew) {
|
|
17
|
-
# Another tray is already running — exit silently
|
|
18
|
-
$mutex.Dispose()
|
|
19
|
-
exit 0
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
# Create tray icon
|
|
23
|
-
$tray = New-Object System.Windows.Forms.NotifyIcon
|
|
24
|
-
|
|
25
|
-
if ($IconPath -and (Test-Path $IconPath)) {
|
|
26
|
-
try {
|
|
27
|
-
$tray.Icon = New-Object System.Drawing.Icon($IconPath)
|
|
28
|
-
} catch {
|
|
29
|
-
$tray.Icon = [System.Drawing.SystemIcons]::Application
|
|
30
|
-
}
|
|
31
|
-
} else {
|
|
32
|
-
$tray.Icon = [System.Drawing.SystemIcons]::Application
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
$tray.Text = "AIMeter v$Version - Tracking AI usage"
|
|
36
|
-
$tray.Visible = $true
|
|
37
|
-
|
|
38
|
-
# Context menu
|
|
39
|
-
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
40
|
-
|
|
41
|
-
$header = $menu.Items.Add("AIMeter Watcher v$Version")
|
|
42
|
-
$header.Enabled = $false
|
|
43
|
-
$header.Font = New-Object System.Drawing.Font($header.Font, [System.Drawing.FontStyle]::Bold)
|
|
44
|
-
|
|
45
|
-
$menu.Items.Add("-")
|
|
46
|
-
|
|
47
|
-
$keyItem = $menu.Items.Add("Key: $MaskedKey")
|
|
48
|
-
$keyItem.Enabled = $false
|
|
49
|
-
|
|
50
|
-
$menu.Items.Add("-")
|
|
51
|
-
|
|
52
|
-
$dashboard = $menu.Items.Add("Open Dashboard")
|
|
53
|
-
$dashboard.Add_Click({ Start-Process "https://getaimeter.com/dashboard" })
|
|
54
|
-
|
|
55
|
-
$logs = $menu.Items.Add("View Logs")
|
|
56
|
-
$viewerScript = Join-Path (Split-Path $MyInvocation.MyCommand.Path) 'log-viewer.ps1'
|
|
57
|
-
$logs.Add_Click({
|
|
58
|
-
# Launch log viewer directly via PowerShell — avoids VBS quoting issues with paths containing spaces
|
|
59
|
-
Start-Process "powershell" -ArgumentList @(
|
|
60
|
-
"-NoProfile",
|
|
61
|
-
"-ExecutionPolicy", "Bypass",
|
|
62
|
-
"-STA",
|
|
63
|
-
"-WindowStyle", "Hidden",
|
|
64
|
-
"-File", $viewerScript,
|
|
65
|
-
"-LogPath", $LogPath
|
|
66
|
-
) -WindowStyle Hidden
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
$menu.Items.Add("-")
|
|
70
|
-
|
|
71
|
-
# ── Help submenu ──────────────────────────────────────────────────────────────
|
|
72
|
-
$helpMenu = New-Object System.Windows.Forms.ToolStripMenuItem("Help")
|
|
73
|
-
|
|
74
|
-
$aboutItem = New-Object System.Windows.Forms.ToolStripMenuItem("About AIMeter")
|
|
75
|
-
$aboutItem.Add_Click({
|
|
76
|
-
[System.Windows.Forms.MessageBox]::Show(
|
|
77
|
-
"AIMeter v$Version`n`nAI coding cost optimizer.`nTrack Claude, Cursor, Codex, and Gemini.`n`nhttps://getaimeter.com",
|
|
78
|
-
"About AIMeter",
|
|
79
|
-
[System.Windows.Forms.MessageBoxButtons]::OK,
|
|
80
|
-
[System.Windows.Forms.MessageBoxIcon]::Information
|
|
81
|
-
)
|
|
82
|
-
})
|
|
83
|
-
$helpMenu.DropDownItems.Add($aboutItem) | Out-Null
|
|
84
|
-
|
|
85
|
-
$checkUpdateItem = New-Object System.Windows.Forms.ToolStripMenuItem("Check for Updates")
|
|
86
|
-
$script:updateJob = $null
|
|
87
|
-
$checkUpdateItem.Add_Click({
|
|
88
|
-
$checkUpdateItem.Text = "Checking..."
|
|
89
|
-
$checkUpdateItem.Enabled = $false
|
|
90
|
-
|
|
91
|
-
# Run HTTP request in a background job to avoid freezing the UI
|
|
92
|
-
$script:updateJob = Start-Job -ScriptBlock {
|
|
93
|
-
try {
|
|
94
|
-
$r = Invoke-RestMethod -Uri "https://registry.npmjs.org/getaimeter/latest" -TimeoutSec 10
|
|
95
|
-
return $r.version
|
|
96
|
-
} catch {
|
|
97
|
-
return "ERROR"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
# Timer polls for job completion (every 500ms)
|
|
102
|
-
$updateTimer = New-Object System.Windows.Forms.Timer
|
|
103
|
-
$updateTimer.Interval = 500
|
|
104
|
-
$updateTimer.Add_Tick({
|
|
105
|
-
if ($script:updateJob.State -eq "Completed") {
|
|
106
|
-
$updateTimer.Stop()
|
|
107
|
-
$updateTimer.Dispose()
|
|
108
|
-
$latest = Receive-Job $script:updateJob
|
|
109
|
-
Remove-Job $script:updateJob
|
|
110
|
-
$script:updateJob = $null
|
|
111
|
-
|
|
112
|
-
$checkUpdateItem.Text = "Check for Updates"
|
|
113
|
-
$checkUpdateItem.Enabled = $true
|
|
114
|
-
|
|
115
|
-
if ($latest -eq "ERROR") {
|
|
116
|
-
[System.Windows.Forms.MessageBox]::Show(
|
|
117
|
-
"Could not check for updates. Please check your internet connection.",
|
|
118
|
-
"Update Check Failed",
|
|
119
|
-
[System.Windows.Forms.MessageBoxButtons]::OK,
|
|
120
|
-
[System.Windows.Forms.MessageBoxIcon]::Warning
|
|
121
|
-
) | Out-Null
|
|
122
|
-
} elseif ($latest -and $latest -ne $Version) {
|
|
123
|
-
$result = [System.Windows.Forms.MessageBox]::Show(
|
|
124
|
-
"Update available: v$Version -> v$latest`n`nClick Yes to open a terminal and run the update command.",
|
|
125
|
-
"Update Available",
|
|
126
|
-
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
|
127
|
-
[System.Windows.Forms.MessageBoxIcon]::Information
|
|
128
|
-
)
|
|
129
|
-
if ($result -eq [System.Windows.Forms.DialogResult]::Yes) {
|
|
130
|
-
# Update and auto-restart: stop watcher, update npm, restart watcher
|
|
131
|
-
$nodePath = (Get-Command node -ErrorAction SilentlyContinue).Source
|
|
132
|
-
$aimeter = (Get-Command aimeter -ErrorAction SilentlyContinue).Source
|
|
133
|
-
Start-Process "cmd.exe" -ArgumentList "/k echo Updating AIMeter... && npm install -g getaimeter@latest && echo. && echo Reinstalling shortcuts... && aimeter install && echo. && echo Updated to latest version! This window can be closed. && echo."
|
|
134
|
-
|
|
135
|
-
# Give the cmd window time to stop us, then exit this tray
|
|
136
|
-
Start-Sleep -Seconds 3
|
|
137
|
-
$tray.Visible = $false
|
|
138
|
-
$tray.Dispose()
|
|
139
|
-
$mutex.ReleaseMutex()
|
|
140
|
-
$mutex.Dispose()
|
|
141
|
-
[System.Windows.Forms.Application]::Exit()
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
[System.Windows.Forms.MessageBox]::Show(
|
|
145
|
-
"You are running the latest version (v$Version).",
|
|
146
|
-
"No Updates",
|
|
147
|
-
[System.Windows.Forms.MessageBoxButtons]::OK,
|
|
148
|
-
[System.Windows.Forms.MessageBoxIcon]::Information
|
|
149
|
-
) | Out-Null
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
$updateTimer.Start()
|
|
154
|
-
})
|
|
155
|
-
$helpMenu.DropDownItems.Add($checkUpdateItem) | Out-Null
|
|
156
|
-
|
|
157
|
-
$helpMenu.DropDownItems.Add("-") | Out-Null
|
|
158
|
-
|
|
159
|
-
$docsItem = New-Object System.Windows.Forms.ToolStripMenuItem("Documentation")
|
|
160
|
-
$docsItem.Add_Click({ Start-Process "https://www.npmjs.com/package/getaimeter" })
|
|
161
|
-
$helpMenu.DropDownItems.Add($docsItem) | Out-Null
|
|
162
|
-
|
|
163
|
-
$websiteItem = New-Object System.Windows.Forms.ToolStripMenuItem("Website")
|
|
164
|
-
$websiteItem.Add_Click({ Start-Process "https://getaimeter.com" })
|
|
165
|
-
$helpMenu.DropDownItems.Add($websiteItem) | Out-Null
|
|
166
|
-
|
|
167
|
-
$menu.Items.Add($helpMenu)
|
|
168
|
-
|
|
169
|
-
$menu.Items.Add("-")
|
|
170
|
-
|
|
171
|
-
$stop = $menu.Items.Add("Stop Watcher")
|
|
172
|
-
$stop.Add_Click({
|
|
173
|
-
$tray.Visible = $false
|
|
174
|
-
$tray.Dispose()
|
|
175
|
-
$mutex.ReleaseMutex()
|
|
176
|
-
$mutex.Dispose()
|
|
177
|
-
if ($ParentPid -gt 0) {
|
|
178
|
-
try { Stop-Process -Id $ParentPid -Force -ErrorAction SilentlyContinue } catch {}
|
|
179
|
-
}
|
|
180
|
-
[System.Windows.Forms.Application]::Exit()
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
$tray.ContextMenuStrip = $menu
|
|
184
|
-
|
|
185
|
-
# Timer to check if parent process is still alive
|
|
186
|
-
$timer = New-Object System.Windows.Forms.Timer
|
|
187
|
-
$timer.Interval = 5000
|
|
188
|
-
$timer.Add_Tick({
|
|
189
|
-
if ($ParentPid -gt 0) {
|
|
190
|
-
try {
|
|
191
|
-
$p = Get-Process -Id $ParentPid -ErrorAction Stop
|
|
192
|
-
} catch {
|
|
193
|
-
# Parent died, clean up
|
|
194
|
-
$tray.Visible = $false
|
|
195
|
-
$tray.Dispose()
|
|
196
|
-
$mutex.ReleaseMutex()
|
|
197
|
-
$mutex.Dispose()
|
|
198
|
-
[System.Windows.Forms.Application]::Exit()
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
$timer.Start()
|
|
203
|
-
|
|
204
|
-
# Double-click tray icon opens dashboard
|
|
205
|
-
$tray.Add_DoubleClick({ Start-Process "https://getaimeter.com/dashboard" })
|
|
206
|
-
|
|
207
|
-
# Run message loop
|
|
208
|
-
[System.Windows.Forms.Application]::Run()
|
|
1
|
+
param(
|
|
2
|
+
[string]$IconPath,
|
|
3
|
+
[string]$MaskedKey,
|
|
4
|
+
[string]$LogPath,
|
|
5
|
+
[int]$ParentPid,
|
|
6
|
+
[string]$Version = "unknown"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
10
|
+
Add-Type -AssemblyName System.Drawing
|
|
11
|
+
|
|
12
|
+
# ── Prevent duplicate tray instances ──────────────────────────────────────────
|
|
13
|
+
$mutexName = "Global\AIMeterTray"
|
|
14
|
+
$createdNew = $false
|
|
15
|
+
$mutex = New-Object System.Threading.Mutex($true, $mutexName, [ref]$createdNew)
|
|
16
|
+
if (-not $createdNew) {
|
|
17
|
+
# Another tray is already running — exit silently
|
|
18
|
+
$mutex.Dispose()
|
|
19
|
+
exit 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Create tray icon
|
|
23
|
+
$tray = New-Object System.Windows.Forms.NotifyIcon
|
|
24
|
+
|
|
25
|
+
if ($IconPath -and (Test-Path $IconPath)) {
|
|
26
|
+
try {
|
|
27
|
+
$tray.Icon = New-Object System.Drawing.Icon($IconPath)
|
|
28
|
+
} catch {
|
|
29
|
+
$tray.Icon = [System.Drawing.SystemIcons]::Application
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
$tray.Icon = [System.Drawing.SystemIcons]::Application
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$tray.Text = "AIMeter v$Version - Tracking AI usage"
|
|
36
|
+
$tray.Visible = $true
|
|
37
|
+
|
|
38
|
+
# Context menu
|
|
39
|
+
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
40
|
+
|
|
41
|
+
$header = $menu.Items.Add("AIMeter Watcher v$Version")
|
|
42
|
+
$header.Enabled = $false
|
|
43
|
+
$header.Font = New-Object System.Drawing.Font($header.Font, [System.Drawing.FontStyle]::Bold)
|
|
44
|
+
|
|
45
|
+
$menu.Items.Add("-")
|
|
46
|
+
|
|
47
|
+
$keyItem = $menu.Items.Add("Key: $MaskedKey")
|
|
48
|
+
$keyItem.Enabled = $false
|
|
49
|
+
|
|
50
|
+
$menu.Items.Add("-")
|
|
51
|
+
|
|
52
|
+
$dashboard = $menu.Items.Add("Open Dashboard")
|
|
53
|
+
$dashboard.Add_Click({ Start-Process "https://getaimeter.com/dashboard" })
|
|
54
|
+
|
|
55
|
+
$logs = $menu.Items.Add("View Logs")
|
|
56
|
+
$viewerScript = Join-Path (Split-Path $MyInvocation.MyCommand.Path) 'log-viewer.ps1'
|
|
57
|
+
$logs.Add_Click({
|
|
58
|
+
# Launch log viewer directly via PowerShell — avoids VBS quoting issues with paths containing spaces
|
|
59
|
+
Start-Process "powershell" -ArgumentList @(
|
|
60
|
+
"-NoProfile",
|
|
61
|
+
"-ExecutionPolicy", "Bypass",
|
|
62
|
+
"-STA",
|
|
63
|
+
"-WindowStyle", "Hidden",
|
|
64
|
+
"-File", $viewerScript,
|
|
65
|
+
"-LogPath", $LogPath
|
|
66
|
+
) -WindowStyle Hidden
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
$menu.Items.Add("-")
|
|
70
|
+
|
|
71
|
+
# ── Help submenu ──────────────────────────────────────────────────────────────
|
|
72
|
+
$helpMenu = New-Object System.Windows.Forms.ToolStripMenuItem("Help")
|
|
73
|
+
|
|
74
|
+
$aboutItem = New-Object System.Windows.Forms.ToolStripMenuItem("About AIMeter")
|
|
75
|
+
$aboutItem.Add_Click({
|
|
76
|
+
[System.Windows.Forms.MessageBox]::Show(
|
|
77
|
+
"AIMeter v$Version`n`nAI coding cost optimizer.`nTrack Claude, Cursor, Codex, and Gemini.`n`nhttps://getaimeter.com",
|
|
78
|
+
"About AIMeter",
|
|
79
|
+
[System.Windows.Forms.MessageBoxButtons]::OK,
|
|
80
|
+
[System.Windows.Forms.MessageBoxIcon]::Information
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
$helpMenu.DropDownItems.Add($aboutItem) | Out-Null
|
|
84
|
+
|
|
85
|
+
$checkUpdateItem = New-Object System.Windows.Forms.ToolStripMenuItem("Check for Updates")
|
|
86
|
+
$script:updateJob = $null
|
|
87
|
+
$checkUpdateItem.Add_Click({
|
|
88
|
+
$checkUpdateItem.Text = "Checking..."
|
|
89
|
+
$checkUpdateItem.Enabled = $false
|
|
90
|
+
|
|
91
|
+
# Run HTTP request in a background job to avoid freezing the UI
|
|
92
|
+
$script:updateJob = Start-Job -ScriptBlock {
|
|
93
|
+
try {
|
|
94
|
+
$r = Invoke-RestMethod -Uri "https://registry.npmjs.org/getaimeter/latest" -TimeoutSec 10
|
|
95
|
+
return $r.version
|
|
96
|
+
} catch {
|
|
97
|
+
return "ERROR"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Timer polls for job completion (every 500ms)
|
|
102
|
+
$updateTimer = New-Object System.Windows.Forms.Timer
|
|
103
|
+
$updateTimer.Interval = 500
|
|
104
|
+
$updateTimer.Add_Tick({
|
|
105
|
+
if ($script:updateJob.State -eq "Completed") {
|
|
106
|
+
$updateTimer.Stop()
|
|
107
|
+
$updateTimer.Dispose()
|
|
108
|
+
$latest = Receive-Job $script:updateJob
|
|
109
|
+
Remove-Job $script:updateJob
|
|
110
|
+
$script:updateJob = $null
|
|
111
|
+
|
|
112
|
+
$checkUpdateItem.Text = "Check for Updates"
|
|
113
|
+
$checkUpdateItem.Enabled = $true
|
|
114
|
+
|
|
115
|
+
if ($latest -eq "ERROR") {
|
|
116
|
+
[System.Windows.Forms.MessageBox]::Show(
|
|
117
|
+
"Could not check for updates. Please check your internet connection.",
|
|
118
|
+
"Update Check Failed",
|
|
119
|
+
[System.Windows.Forms.MessageBoxButtons]::OK,
|
|
120
|
+
[System.Windows.Forms.MessageBoxIcon]::Warning
|
|
121
|
+
) | Out-Null
|
|
122
|
+
} elseif ($latest -and $latest -ne $Version) {
|
|
123
|
+
$result = [System.Windows.Forms.MessageBox]::Show(
|
|
124
|
+
"Update available: v$Version -> v$latest`n`nClick Yes to open a terminal and run the update command.",
|
|
125
|
+
"Update Available",
|
|
126
|
+
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
|
127
|
+
[System.Windows.Forms.MessageBoxIcon]::Information
|
|
128
|
+
)
|
|
129
|
+
if ($result -eq [System.Windows.Forms.DialogResult]::Yes) {
|
|
130
|
+
# Update and auto-restart: stop watcher, update npm, restart watcher
|
|
131
|
+
$nodePath = (Get-Command node -ErrorAction SilentlyContinue).Source
|
|
132
|
+
$aimeter = (Get-Command aimeter -ErrorAction SilentlyContinue).Source
|
|
133
|
+
Start-Process "cmd.exe" -ArgumentList "/k echo Updating AIMeter... && npm install -g getaimeter@latest && echo. && echo Reinstalling shortcuts... && aimeter install && echo. && echo Updated to latest version! This window can be closed. && echo."
|
|
134
|
+
|
|
135
|
+
# Give the cmd window time to stop us, then exit this tray
|
|
136
|
+
Start-Sleep -Seconds 3
|
|
137
|
+
$tray.Visible = $false
|
|
138
|
+
$tray.Dispose()
|
|
139
|
+
$mutex.ReleaseMutex()
|
|
140
|
+
$mutex.Dispose()
|
|
141
|
+
[System.Windows.Forms.Application]::Exit()
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
[System.Windows.Forms.MessageBox]::Show(
|
|
145
|
+
"You are running the latest version (v$Version).",
|
|
146
|
+
"No Updates",
|
|
147
|
+
[System.Windows.Forms.MessageBoxButtons]::OK,
|
|
148
|
+
[System.Windows.Forms.MessageBoxIcon]::Information
|
|
149
|
+
) | Out-Null
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
$updateTimer.Start()
|
|
154
|
+
})
|
|
155
|
+
$helpMenu.DropDownItems.Add($checkUpdateItem) | Out-Null
|
|
156
|
+
|
|
157
|
+
$helpMenu.DropDownItems.Add("-") | Out-Null
|
|
158
|
+
|
|
159
|
+
$docsItem = New-Object System.Windows.Forms.ToolStripMenuItem("Documentation")
|
|
160
|
+
$docsItem.Add_Click({ Start-Process "https://www.npmjs.com/package/getaimeter" })
|
|
161
|
+
$helpMenu.DropDownItems.Add($docsItem) | Out-Null
|
|
162
|
+
|
|
163
|
+
$websiteItem = New-Object System.Windows.Forms.ToolStripMenuItem("Website")
|
|
164
|
+
$websiteItem.Add_Click({ Start-Process "https://getaimeter.com" })
|
|
165
|
+
$helpMenu.DropDownItems.Add($websiteItem) | Out-Null
|
|
166
|
+
|
|
167
|
+
$menu.Items.Add($helpMenu)
|
|
168
|
+
|
|
169
|
+
$menu.Items.Add("-")
|
|
170
|
+
|
|
171
|
+
$stop = $menu.Items.Add("Stop Watcher")
|
|
172
|
+
$stop.Add_Click({
|
|
173
|
+
$tray.Visible = $false
|
|
174
|
+
$tray.Dispose()
|
|
175
|
+
$mutex.ReleaseMutex()
|
|
176
|
+
$mutex.Dispose()
|
|
177
|
+
if ($ParentPid -gt 0) {
|
|
178
|
+
try { Stop-Process -Id $ParentPid -Force -ErrorAction SilentlyContinue } catch {}
|
|
179
|
+
}
|
|
180
|
+
[System.Windows.Forms.Application]::Exit()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
$tray.ContextMenuStrip = $menu
|
|
184
|
+
|
|
185
|
+
# Timer to check if parent process is still alive
|
|
186
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
187
|
+
$timer.Interval = 5000
|
|
188
|
+
$timer.Add_Tick({
|
|
189
|
+
if ($ParentPid -gt 0) {
|
|
190
|
+
try {
|
|
191
|
+
$p = Get-Process -Id $ParentPid -ErrorAction Stop
|
|
192
|
+
} catch {
|
|
193
|
+
# Parent died, clean up
|
|
194
|
+
$tray.Visible = $false
|
|
195
|
+
$tray.Dispose()
|
|
196
|
+
$mutex.ReleaseMutex()
|
|
197
|
+
$mutex.Dispose()
|
|
198
|
+
[System.Windows.Forms.Application]::Exit()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
$timer.Start()
|
|
203
|
+
|
|
204
|
+
# Double-click tray icon opens dashboard
|
|
205
|
+
$tray.Add_DoubleClick({ Start-Process "https://getaimeter.com/dashboard" })
|
|
206
|
+
|
|
207
|
+
# Run message loop
|
|
208
|
+
[System.Windows.Forms.Application]::Run()
|
package/watcher.js
CHANGED
|
@@ -332,6 +332,30 @@ function extractNewUsage(filePath) {
|
|
|
332
332
|
continue;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
// ── Copilot VS Code agent: real-time per-turn output tracking ──
|
|
336
|
+
// assistant.message fires after every AI response with data.outputTokens.
|
|
337
|
+
// Input tokens are not available per-turn — they come from session.shutdown.
|
|
338
|
+
if (obj.type === 'assistant.message' && obj.data?.outputTokens > 0) {
|
|
339
|
+
const msgId = obj.data.messageId;
|
|
340
|
+
const hashKey = `${filePath}:copilot-msg:${msgId}`;
|
|
341
|
+
const hash = crypto.createHash('md5').update(hashKey).digest('hex');
|
|
342
|
+
if (isDuplicate(hash)) continue;
|
|
343
|
+
|
|
344
|
+
usageEvents.push({
|
|
345
|
+
provider: 'github',
|
|
346
|
+
model: 'copilot',
|
|
347
|
+
source: detectSource(filePath),
|
|
348
|
+
inputTokens: 0,
|
|
349
|
+
outputTokens: obj.data.outputTokens,
|
|
350
|
+
thinkingTokens: 0,
|
|
351
|
+
cacheReadTokens: 0,
|
|
352
|
+
cacheWriteTokens: 0,
|
|
353
|
+
conversationId: convMeta.conversationId,
|
|
354
|
+
projectPath: convMeta.projectPath,
|
|
355
|
+
});
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
335
359
|
// ── Copilot format (old: token_usage, new: session.shutdown with modelMetrics) ──
|
|
336
360
|
if (obj.type === 'token_usage' && (obj.input_tokens !== undefined || obj.output_tokens !== undefined)) {
|
|
337
361
|
const copilotModel = obj.model || 'copilot';
|
|
@@ -354,15 +378,19 @@ function extractNewUsage(filePath) {
|
|
|
354
378
|
continue;
|
|
355
379
|
}
|
|
356
380
|
|
|
357
|
-
// GitHub Copilot agent: session.shutdown
|
|
381
|
+
// GitHub Copilot agent: session.shutdown reports the per-model session totals.
|
|
382
|
+
// Output tokens were already reported turn-by-turn via assistant.message above,
|
|
383
|
+
// so here we only emit input tokens (+ cache) to avoid double-counting output.
|
|
358
384
|
if (obj.type === 'session.shutdown' && obj.data?.modelMetrics) {
|
|
359
385
|
for (const [model, metrics] of Object.entries(obj.data.modelMetrics)) {
|
|
360
386
|
const u = metrics.usage || {};
|
|
361
|
-
const inputTokens
|
|
362
|
-
const
|
|
363
|
-
|
|
387
|
+
const inputTokens = u.inputTokens || 0;
|
|
388
|
+
const cacheReadTokens = u.cacheReadTokens || 0;
|
|
389
|
+
const cacheWriteTokens = u.cacheWriteTokens || 0;
|
|
390
|
+
// Skip if no input data (output was already handled per-turn)
|
|
391
|
+
if (inputTokens === 0 && cacheReadTokens === 0) continue;
|
|
364
392
|
|
|
365
|
-
const hashKey = `${filePath}:copilot-shutdown:${model}:${inputTokens}
|
|
393
|
+
const hashKey = `${filePath}:copilot-shutdown-input:${model}:${inputTokens}`;
|
|
366
394
|
const hash = crypto.createHash('md5').update(hashKey).digest('hex');
|
|
367
395
|
if (isDuplicate(hash)) continue;
|
|
368
396
|
|
|
@@ -377,10 +405,10 @@ function extractNewUsage(filePath) {
|
|
|
377
405
|
model,
|
|
378
406
|
source: detectSource(filePath),
|
|
379
407
|
inputTokens,
|
|
380
|
-
outputTokens,
|
|
408
|
+
outputTokens: 0, // already reported per assistant.message turn
|
|
381
409
|
thinkingTokens: 0,
|
|
382
|
-
cacheReadTokens
|
|
383
|
-
cacheWriteTokens
|
|
410
|
+
cacheReadTokens,
|
|
411
|
+
cacheWriteTokens,
|
|
384
412
|
conversationId: convMeta.conversationId,
|
|
385
413
|
projectPath: convMeta.projectPath,
|
|
386
414
|
});
|