getaimeter 0.2.1 → 0.3.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 +12 -0
- package/icon.ico +0 -0
- package/package.json +6 -4
- package/tray-launcher.vbs +2 -0
- package/tray.js +38 -0
- package/tray.ps1 +79 -0
- package/watcher.js +19 -0
package/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ const { getApiKey, saveApiKey, getWatchPaths, AIMETER_DIR } = require('./config'
|
|
|
5
5
|
const { startWatching } = require('./watcher');
|
|
6
6
|
const { install, uninstall, isInstalled, startNow, stopNow } = require('./service');
|
|
7
7
|
const { checkForUpdate, getCurrentVersion } = require('./update-check');
|
|
8
|
+
const { startTray, stopTray } = require('./tray');
|
|
8
9
|
|
|
9
10
|
const command = process.argv[2] || 'help';
|
|
10
11
|
|
|
@@ -187,8 +188,18 @@ function runWatch() {
|
|
|
187
188
|
|
|
188
189
|
const cleanup = startWatching();
|
|
189
190
|
|
|
191
|
+
// Launch system tray icon (non-blocking, fails silently if systray2 not available)
|
|
192
|
+
const showTray = !process.argv.includes('--no-tray');
|
|
193
|
+
if (showTray) {
|
|
194
|
+
startTray(() => {
|
|
195
|
+
cleanup();
|
|
196
|
+
try { fs.unlinkSync(lockFile); } catch {}
|
|
197
|
+
}).catch(() => {}); // ignore tray errors
|
|
198
|
+
}
|
|
199
|
+
|
|
190
200
|
const cleanupAll = () => {
|
|
191
201
|
cleanup();
|
|
202
|
+
stopTray();
|
|
192
203
|
try { fs.unlinkSync(lockFile); } catch {}
|
|
193
204
|
process.exit(0);
|
|
194
205
|
};
|
|
@@ -196,6 +207,7 @@ function runWatch() {
|
|
|
196
207
|
process.on('SIGINT', cleanupAll);
|
|
197
208
|
process.on('SIGTERM', cleanupAll);
|
|
198
209
|
process.on('exit', () => {
|
|
210
|
+
stopTray();
|
|
199
211
|
try { fs.unlinkSync(lockFile); } catch {}
|
|
200
212
|
});
|
|
201
213
|
|
package/icon.ico
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getaimeter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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"
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"config.js",
|
|
30
30
|
"state.js",
|
|
31
31
|
"service.js",
|
|
32
|
+
"tray.js",
|
|
33
|
+
"tray.ps1",
|
|
34
|
+
"tray-launcher.vbs",
|
|
35
|
+
"icon.ico",
|
|
32
36
|
"update-check.js",
|
|
33
37
|
"README.md"
|
|
34
38
|
],
|
|
@@ -39,7 +43,5 @@
|
|
|
39
43
|
"homepage": "https://getaimeter.com",
|
|
40
44
|
"author": "Alejandro Ceja",
|
|
41
45
|
"preferGlobal": true,
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"systray2": "^2.1.4"
|
|
44
|
-
}
|
|
46
|
+
"dependencies": {}
|
|
45
47
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Set objShell = CreateObject("WScript.Shell")
|
|
2
|
+
objShell.Run "powershell -NoProfile -ExecutionPolicy Bypass -STA -File """ & Replace(WScript.ScriptFullName, "tray-launcher.vbs", "tray.ps1") & """ -IconPath """ & Replace(WScript.ScriptFullName, "tray-launcher.vbs", "icon.ico") & """ -MaskedKey """ & WScript.Arguments(0) & """ -LogPath """ & WScript.Arguments(1) & """ -ParentPid " & WScript.Arguments(2), 0, False
|
package/tray.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
const { getApiKey, AIMETER_DIR } = require('./config');
|
|
6
|
+
|
|
7
|
+
let trayPid = null;
|
|
8
|
+
|
|
9
|
+
async function startTray(onStop) {
|
|
10
|
+
if (process.platform !== 'win32') return null;
|
|
11
|
+
|
|
12
|
+
const vbsPath = path.join(__dirname, 'tray-launcher.vbs');
|
|
13
|
+
const apiKey = getApiKey();
|
|
14
|
+
const maskedKey = apiKey ? `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}` : 'not set';
|
|
15
|
+
const logPath = path.join(AIMETER_DIR, 'watcher.log');
|
|
16
|
+
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
// Use cscript to launch VBS which launches PowerShell hidden with -STA
|
|
19
|
+
execFile('cscript', [
|
|
20
|
+
'//Nologo',
|
|
21
|
+
vbsPath,
|
|
22
|
+
maskedKey,
|
|
23
|
+
logPath,
|
|
24
|
+
String(process.pid),
|
|
25
|
+
], { windowsHide: true }, (err) => {
|
|
26
|
+
if (err) resolve(null);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// VBS returns immediately, give PS time to start
|
|
30
|
+
setTimeout(() => resolve(true), 3000);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stopTray() {
|
|
35
|
+
// The tray PS1 watches parent PID and self-terminates
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { startTray, stopTray };
|
package/tray.ps1
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$IconPath,
|
|
3
|
+
[string]$MaskedKey,
|
|
4
|
+
[string]$LogPath,
|
|
5
|
+
[int]$ParentPid
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
9
|
+
Add-Type -AssemblyName System.Drawing
|
|
10
|
+
|
|
11
|
+
# Create tray icon
|
|
12
|
+
$tray = New-Object System.Windows.Forms.NotifyIcon
|
|
13
|
+
|
|
14
|
+
if ($IconPath -and (Test-Path $IconPath)) {
|
|
15
|
+
try {
|
|
16
|
+
$tray.Icon = New-Object System.Drawing.Icon($IconPath)
|
|
17
|
+
} catch {
|
|
18
|
+
$tray.Icon = [System.Drawing.SystemIcons]::Application
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
$tray.Icon = [System.Drawing.SystemIcons]::Application
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
$tray.Text = "AIMeter - Tracking AI usage"
|
|
25
|
+
$tray.Visible = $true
|
|
26
|
+
|
|
27
|
+
# Context menu
|
|
28
|
+
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
29
|
+
|
|
30
|
+
$header = $menu.Items.Add("AIMeter Watcher")
|
|
31
|
+
$header.Enabled = $false
|
|
32
|
+
$header.Font = New-Object System.Drawing.Font($header.Font, [System.Drawing.FontStyle]::Bold)
|
|
33
|
+
|
|
34
|
+
$menu.Items.Add("-")
|
|
35
|
+
|
|
36
|
+
$keyItem = $menu.Items.Add("Key: $MaskedKey")
|
|
37
|
+
$keyItem.Enabled = $false
|
|
38
|
+
|
|
39
|
+
$menu.Items.Add("-")
|
|
40
|
+
|
|
41
|
+
$dashboard = $menu.Items.Add("Open Dashboard")
|
|
42
|
+
$dashboard.Add_Click({ Start-Process "https://getaimeter.com/dashboard" })
|
|
43
|
+
|
|
44
|
+
$logs = $menu.Items.Add("View Logs")
|
|
45
|
+
$logs.Add_Click({ Start-Process notepad $LogPath })
|
|
46
|
+
|
|
47
|
+
$menu.Items.Add("-")
|
|
48
|
+
|
|
49
|
+
$stop = $menu.Items.Add("Stop Watcher")
|
|
50
|
+
$stop.Add_Click({
|
|
51
|
+
$tray.Visible = $false
|
|
52
|
+
$tray.Dispose()
|
|
53
|
+
if ($ParentPid -gt 0) {
|
|
54
|
+
try { Stop-Process -Id $ParentPid -Force -ErrorAction SilentlyContinue } catch {}
|
|
55
|
+
}
|
|
56
|
+
[System.Windows.Forms.Application]::Exit()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
$tray.ContextMenuStrip = $menu
|
|
60
|
+
|
|
61
|
+
# Timer to check if parent process is still alive
|
|
62
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
63
|
+
$timer.Interval = 5000
|
|
64
|
+
$timer.Add_Tick({
|
|
65
|
+
if ($ParentPid -gt 0) {
|
|
66
|
+
try {
|
|
67
|
+
$p = Get-Process -Id $ParentPid -ErrorAction Stop
|
|
68
|
+
} catch {
|
|
69
|
+
# Parent died, clean up
|
|
70
|
+
$tray.Visible = $false
|
|
71
|
+
$tray.Dispose()
|
|
72
|
+
[System.Windows.Forms.Application]::Exit()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
$timer.Start()
|
|
77
|
+
|
|
78
|
+
# Run message loop
|
|
79
|
+
[System.Windows.Forms.Application]::Run()
|
package/watcher.js
CHANGED
|
@@ -43,6 +43,25 @@ function detectSource(filePath) {
|
|
|
43
43
|
return 'desktop_app';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// For subagent files (e.g. .../ceeb9217.../subagents/agent-xxx.jsonl),
|
|
47
|
+
// inherit the parent session's source. Parent JSONL is at the grandparent dir level.
|
|
48
|
+
if (normalized.includes('/subagents/')) {
|
|
49
|
+
// Extract session UUID dir: .../projects/PROJECT/SESSION_UUID/subagents/...
|
|
50
|
+
const match = normalized.match(/(.+)\/([^/]+)\/subagents\//);
|
|
51
|
+
if (match) {
|
|
52
|
+
const projectDir = match[1]; // .../projects/PROJECT
|
|
53
|
+
const sessionUuid = match[2]; // SESSION_UUID
|
|
54
|
+
const parentFile = path.join(projectDir.replace(/\//g, path.sep), sessionUuid + '.jsonl');
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(parentFile)) {
|
|
57
|
+
const parentSource = detectSource(parentFile);
|
|
58
|
+
_sourceCache.set(filePath, parentSource);
|
|
59
|
+
return parentSource;
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
// Read first 10KB of the file to find entrypoint or IDE markers
|
|
47
66
|
let source = 'cli'; // default
|
|
48
67
|
try {
|