getaimeter 0.10.0 → 0.11.1
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/config.js +6 -7
- package/package.json +1 -1
- package/tray.ps1 +208 -208
- package/watcher.js +47 -8
package/config.js
CHANGED
|
@@ -68,13 +68,12 @@ function getWatchPaths() {
|
|
|
68
68
|
}
|
|
69
69
|
if (fs.existsSync(desktopSessions)) paths.push(desktopSessions);
|
|
70
70
|
|
|
71
|
-
// 3. OpenAI Codex CLI
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (fs.existsSync(codexHistory) && !paths.includes(codexHistory)) paths.push(codexHistory);
|
|
71
|
+
// 3. OpenAI Codex CLI — watch the root ~/.codex dir so findJsonlFiles recurses
|
|
72
|
+
// into both sessions/ (per-session UUIDs) and history.jsonl (single flat file).
|
|
73
|
+
// Previously both ~/.codex and ~/.codex/sessions were added separately, causing
|
|
74
|
+
// the same files to be discovered twice per poll cycle.
|
|
75
|
+
const codexDir = path.join(os.homedir(), '.codex');
|
|
76
|
+
if (fs.existsSync(codexDir)) paths.push(codexDir);
|
|
78
77
|
|
|
79
78
|
// 4. GitHub Copilot CLI sessions
|
|
80
79
|
const copilotSessions = path.join(os.homedir(), '.copilot', 'session-state');
|
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
|
@@ -162,6 +162,17 @@ function getConversationMeta(filePath) {
|
|
|
162
162
|
// Conversation ID = file basename without extension
|
|
163
163
|
let conversationId = path.basename(filePath, '.jsonl');
|
|
164
164
|
|
|
165
|
+
// Copilot stores sessions as <session-UUID>/events.jsonl — all files are named
|
|
166
|
+
// "events.jsonl", so the basename is always "events" which collides across sessions.
|
|
167
|
+
// Use the parent directory UUID as the conversation ID instead.
|
|
168
|
+
if (conversationId === 'events') {
|
|
169
|
+
const parentDir = path.basename(path.dirname(filePath));
|
|
170
|
+
// Only override if parent looks like a UUID (avoids false positives)
|
|
171
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(parentDir)) {
|
|
172
|
+
conversationId = parentDir;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
// For subagent files, use the parent session UUID as conversation ID
|
|
166
177
|
const subagentMatch = normalized.match(/\/([^/]+)\/subagents\//);
|
|
167
178
|
if (subagentMatch) {
|
|
@@ -332,6 +343,30 @@ function extractNewUsage(filePath) {
|
|
|
332
343
|
continue;
|
|
333
344
|
}
|
|
334
345
|
|
|
346
|
+
// ── Copilot VS Code agent: real-time per-turn output tracking ──
|
|
347
|
+
// assistant.message fires after every AI response with data.outputTokens.
|
|
348
|
+
// Input tokens are not available per-turn — they come from session.shutdown.
|
|
349
|
+
if (obj.type === 'assistant.message' && obj.data?.outputTokens > 0) {
|
|
350
|
+
const msgId = obj.data.messageId;
|
|
351
|
+
const hashKey = `${filePath}:copilot-msg:${msgId}`;
|
|
352
|
+
const hash = crypto.createHash('md5').update(hashKey).digest('hex');
|
|
353
|
+
if (isDuplicate(hash)) continue;
|
|
354
|
+
|
|
355
|
+
usageEvents.push({
|
|
356
|
+
provider: 'github',
|
|
357
|
+
model: 'copilot',
|
|
358
|
+
source: detectSource(filePath),
|
|
359
|
+
inputTokens: 0,
|
|
360
|
+
outputTokens: obj.data.outputTokens,
|
|
361
|
+
thinkingTokens: 0,
|
|
362
|
+
cacheReadTokens: 0,
|
|
363
|
+
cacheWriteTokens: 0,
|
|
364
|
+
conversationId: convMeta.conversationId,
|
|
365
|
+
projectPath: convMeta.projectPath,
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
335
370
|
// ── Copilot format (old: token_usage, new: session.shutdown with modelMetrics) ──
|
|
336
371
|
if (obj.type === 'token_usage' && (obj.input_tokens !== undefined || obj.output_tokens !== undefined)) {
|
|
337
372
|
const copilotModel = obj.model || 'copilot';
|
|
@@ -354,15 +389,19 @@ function extractNewUsage(filePath) {
|
|
|
354
389
|
continue;
|
|
355
390
|
}
|
|
356
391
|
|
|
357
|
-
// GitHub Copilot agent: session.shutdown
|
|
392
|
+
// GitHub Copilot agent: session.shutdown reports the per-model session totals.
|
|
393
|
+
// Output tokens were already reported turn-by-turn via assistant.message above,
|
|
394
|
+
// so here we only emit input tokens (+ cache) to avoid double-counting output.
|
|
358
395
|
if (obj.type === 'session.shutdown' && obj.data?.modelMetrics) {
|
|
359
396
|
for (const [model, metrics] of Object.entries(obj.data.modelMetrics)) {
|
|
360
397
|
const u = metrics.usage || {};
|
|
361
|
-
const inputTokens
|
|
362
|
-
const
|
|
363
|
-
|
|
398
|
+
const inputTokens = u.inputTokens || 0;
|
|
399
|
+
const cacheReadTokens = u.cacheReadTokens || 0;
|
|
400
|
+
const cacheWriteTokens = u.cacheWriteTokens || 0;
|
|
401
|
+
// Skip if no input data (output was already handled per-turn)
|
|
402
|
+
if (inputTokens === 0 && cacheReadTokens === 0) continue;
|
|
364
403
|
|
|
365
|
-
const hashKey = `${filePath}:copilot-shutdown:${model}:${inputTokens}
|
|
404
|
+
const hashKey = `${filePath}:copilot-shutdown-input:${model}:${inputTokens}`;
|
|
366
405
|
const hash = crypto.createHash('md5').update(hashKey).digest('hex');
|
|
367
406
|
if (isDuplicate(hash)) continue;
|
|
368
407
|
|
|
@@ -377,10 +416,10 @@ function extractNewUsage(filePath) {
|
|
|
377
416
|
model,
|
|
378
417
|
source: detectSource(filePath),
|
|
379
418
|
inputTokens,
|
|
380
|
-
outputTokens,
|
|
419
|
+
outputTokens: 0, // already reported per assistant.message turn
|
|
381
420
|
thinkingTokens: 0,
|
|
382
|
-
cacheReadTokens
|
|
383
|
-
cacheWriteTokens
|
|
421
|
+
cacheReadTokens,
|
|
422
|
+
cacheWriteTokens,
|
|
384
423
|
conversationId: convMeta.conversationId,
|
|
385
424
|
projectPath: convMeta.projectPath,
|
|
386
425
|
});
|