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 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.2.1",
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 {