getaimeter 0.7.0 → 0.7.2
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 +2 -0
- package/package.json +1 -1
- package/service.js +54 -1
- package/watcher.js +37 -34
package/cli.js
CHANGED
|
@@ -129,6 +129,8 @@ async function runSetup() {
|
|
|
129
129
|
console.log(` ✓ Installed at: ${result.path}`);
|
|
130
130
|
if (result.platform === 'windows') {
|
|
131
131
|
console.log(' ✓ Will auto-start on login (Windows Startup folder)');
|
|
132
|
+
console.log(' ✓ Desktop shortcut created (double-click "AIMeter" to start)');
|
|
133
|
+
console.log(' ✓ Start Menu shortcut created');
|
|
132
134
|
} else if (result.platform === 'macos') {
|
|
133
135
|
console.log(' ✓ Will auto-start on login (launchd)');
|
|
134
136
|
} else {
|
package/package.json
CHANGED
package/service.js
CHANGED
|
@@ -86,12 +86,65 @@ WshShell.Run """${nodePath}"" ""${script}"" watch", 0, False
|
|
|
86
86
|
|
|
87
87
|
fs.mkdirSync(path.dirname(vbsPath), { recursive: true });
|
|
88
88
|
fs.writeFileSync(vbsPath, vbs, 'utf8');
|
|
89
|
+
|
|
90
|
+
// Create Desktop and Start Menu shortcuts so users can launch without a terminal
|
|
91
|
+
createWindowsShortcuts(nodePath, script);
|
|
92
|
+
|
|
89
93
|
return vbsPath;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Create .lnk shortcuts on Desktop and Start Menu via PowerShell COM.
|
|
98
|
+
* These are proper Windows shortcuts with icon and description.
|
|
99
|
+
*/
|
|
100
|
+
function createWindowsShortcuts(nodePath, script) {
|
|
101
|
+
const iconPath = path.join(__dirname, 'icon.ico').replace(/\\/g, '\\\\');
|
|
102
|
+
const locations = [
|
|
103
|
+
{ name: 'Desktop', dir: path.join(os.homedir(), 'Desktop') },
|
|
104
|
+
{ name: 'Start Menu', dir: path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs') },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const loc of locations) {
|
|
108
|
+
const lnkPath = path.join(loc.dir, 'AIMeter.lnk');
|
|
109
|
+
// PowerShell script to create a .lnk shortcut
|
|
110
|
+
const ps = `
|
|
111
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
112
|
+
$s = $ws.CreateShortcut('${lnkPath.replace(/'/g, "''")}')
|
|
113
|
+
$s.TargetPath = '${nodePath.replace(/\\\\/g, '\\').replace(/'/g, "''")}'
|
|
114
|
+
$s.Arguments = '"${script.replace(/\\\\/g, '\\').replace(/'/g, "''")}" start'
|
|
115
|
+
$s.WorkingDirectory = '${os.homedir().replace(/'/g, "''")}'
|
|
116
|
+
$s.Description = 'AIMeter - AI coding cost optimizer'
|
|
117
|
+
$s.WindowStyle = 7
|
|
118
|
+
$iconFile = '${iconPath.replace(/\\\\/g, '\\').replace(/'/g, "''")}'
|
|
119
|
+
if (Test-Path $iconFile) { $s.IconLocation = $iconFile }
|
|
120
|
+
$s.Save()
|
|
121
|
+
`.trim();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
require('child_process').execSync(
|
|
125
|
+
`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps.replace(/"/g, '\\"')}"`,
|
|
126
|
+
{ stdio: 'ignore', timeout: 10000 }
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
// Non-critical — shortcuts are convenience, not required
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
92
134
|
function uninstallWindows() {
|
|
93
135
|
const vbsPath = getWindowsVbsPath();
|
|
94
|
-
try { fs.unlinkSync(vbsPath);
|
|
136
|
+
try { fs.unlinkSync(vbsPath); } catch {}
|
|
137
|
+
|
|
138
|
+
// Remove Desktop and Start Menu shortcuts
|
|
139
|
+
const shortcuts = [
|
|
140
|
+
path.join(os.homedir(), 'Desktop', 'AIMeter.lnk'),
|
|
141
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'AIMeter.lnk'),
|
|
142
|
+
];
|
|
143
|
+
for (const lnk of shortcuts) {
|
|
144
|
+
try { fs.unlinkSync(lnk); } catch {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return true;
|
|
95
148
|
}
|
|
96
149
|
|
|
97
150
|
function isInstalledWindows() {
|
package/watcher.js
CHANGED
|
@@ -310,8 +310,9 @@ function extractNewUsage(filePath) {
|
|
|
310
310
|
// Cursor IDE — extract usage from SQLite state.vscdb
|
|
311
311
|
// ---------------------------------------------------------------------------
|
|
312
312
|
|
|
313
|
-
// Track last-seen
|
|
314
|
-
|
|
313
|
+
// Track last-seen bubble counts per conversation to detect new messages
|
|
314
|
+
// Map<composerId, { aiCount, aiTextLen, userTextLen }>
|
|
315
|
+
const _cursorConvState = new Map();
|
|
315
316
|
let _cursorSqliteAvailable = null; // null = unchecked, true/false after first check
|
|
316
317
|
|
|
317
318
|
// Locate sqlite3 binary — may be in PATH or in the WinGet install location
|
|
@@ -413,7 +414,6 @@ function extractCursorUsage(dbPath) {
|
|
|
413
414
|
const parts = line.split('|');
|
|
414
415
|
if (parts.length < 4) continue;
|
|
415
416
|
const [key, composerId, modelName, createdAt] = parts;
|
|
416
|
-
if (_cursorSeenKeys.has(key)) continue;
|
|
417
417
|
conversations.push({ key, composerId, modelName: modelName || 'default', createdAt: parseInt(createdAt) || 0 });
|
|
418
418
|
}
|
|
419
419
|
|
|
@@ -422,7 +422,8 @@ function extractCursorUsage(dbPath) {
|
|
|
422
422
|
return [];
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
// Step 2: For
|
|
425
|
+
// Step 2: For each conversation, check if bubble count increased since last check.
|
|
426
|
+
// Report only the DELTA (new messages since last poll).
|
|
426
427
|
for (const conv of conversations) {
|
|
427
428
|
try {
|
|
428
429
|
// Count AI responses (type=2) and sum their text lengths
|
|
@@ -430,19 +431,11 @@ function extractCursorUsage(dbPath) {
|
|
|
430
431
|
`SELECT count(*), sum(length(json_extract(value, '$.text'))) FROM cursorDiskKV WHERE key LIKE 'bubbleId:${conv.composerId}:%' AND json_extract(value, '$.type') = 2`
|
|
431
432
|
);
|
|
432
433
|
|
|
433
|
-
if (!bubbleRaw || !bubbleRaw.trim())
|
|
434
|
-
_cursorSeenKeys.add(conv.key);
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
434
|
+
if (!bubbleRaw || !bubbleRaw.trim()) continue;
|
|
437
435
|
|
|
438
436
|
const [countStr, textLenStr] = bubbleRaw.trim().split('|');
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
if (responseCount === 0) {
|
|
443
|
-
_cursorSeenKeys.add(conv.key);
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
437
|
+
const aiCount = parseInt(countStr) || 0;
|
|
438
|
+
const aiTextLen = parseInt(textLenStr) || 0;
|
|
446
439
|
|
|
447
440
|
// Also count user messages for input estimation
|
|
448
441
|
const userRaw = runSqlite(dbPath,
|
|
@@ -450,21 +443,31 @@ function extractCursorUsage(dbPath) {
|
|
|
450
443
|
);
|
|
451
444
|
const userTextLen = parseInt((userRaw || '').trim()) || 0;
|
|
452
445
|
|
|
453
|
-
//
|
|
454
|
-
const
|
|
455
|
-
const estimatedOutputTokens = Math.ceil(totalTextLen / 4);
|
|
446
|
+
// Get previous state for this conversation
|
|
447
|
+
const prev = _cursorConvState.get(conv.composerId) || { aiCount: 0, aiTextLen: 0, userTextLen: 0 };
|
|
456
448
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
continue;
|
|
449
|
+
// Check if anything changed
|
|
450
|
+
if (aiCount <= prev.aiCount && aiTextLen <= prev.aiTextLen && userTextLen <= prev.userTextLen) {
|
|
451
|
+
continue; // No new messages
|
|
460
452
|
}
|
|
461
453
|
|
|
462
|
-
|
|
454
|
+
// Calculate delta (new tokens since last check)
|
|
455
|
+
const deltaAiTextLen = Math.max(0, aiTextLen - prev.aiTextLen);
|
|
456
|
+
const deltaUserTextLen = Math.max(0, userTextLen - prev.userTextLen);
|
|
457
|
+
|
|
458
|
+
// Update stored state
|
|
459
|
+
_cursorConvState.set(conv.composerId, { aiCount, aiTextLen, userTextLen });
|
|
460
|
+
|
|
461
|
+
// Estimate tokens: ~4 chars per token
|
|
462
|
+
const estimatedInputTokens = Math.ceil(deltaUserTextLen / 4);
|
|
463
|
+
const estimatedOutputTokens = Math.ceil(deltaAiTextLen / 4);
|
|
464
|
+
|
|
465
|
+
if (estimatedInputTokens === 0 && estimatedOutputTokens === 0) continue;
|
|
466
|
+
|
|
467
|
+
// Dedup by exact delta values
|
|
468
|
+
const hashKey = `cursor:${conv.composerId}:${aiCount}:${aiTextLen}:${userTextLen}`;
|
|
463
469
|
const hash = crypto.createHash('md5').update(hashKey).digest('hex');
|
|
464
|
-
if (isDuplicate(hash))
|
|
465
|
-
_cursorSeenKeys.add(conv.key);
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
470
|
+
if (isDuplicate(hash)) continue;
|
|
468
471
|
|
|
469
472
|
// Determine provider from model name
|
|
470
473
|
let provider = 'openai';
|
|
@@ -487,8 +490,6 @@ function extractCursorUsage(dbPath) {
|
|
|
487
490
|
cacheWriteTokens: 0,
|
|
488
491
|
});
|
|
489
492
|
} catch {}
|
|
490
|
-
|
|
491
|
-
_cursorSeenKeys.add(conv.key);
|
|
492
493
|
}
|
|
493
494
|
} catch (err) {
|
|
494
495
|
if (!err.message.includes('database is locked')) {
|
|
@@ -654,20 +655,22 @@ function startWatching() {
|
|
|
654
655
|
const cursorDbs = findCursorDbs(watchPaths);
|
|
655
656
|
if (cursorDbs.length > 0) {
|
|
656
657
|
log('Cursor databases found:', cursorDbs.join(', '));
|
|
657
|
-
//
|
|
658
|
-
//
|
|
658
|
+
// Pre-populate _cursorConvState on startup so we only report new messages.
|
|
659
|
+
// On first run, set the mtime offset and discard results.
|
|
660
|
+
// On subsequent runs, report any new data as catch-up.
|
|
659
661
|
for (const dbPath of cursorDbs) {
|
|
660
662
|
try {
|
|
661
663
|
if (isFirstRun) {
|
|
662
|
-
|
|
664
|
+
// Set WAL mtime so first poll doesn't re-process
|
|
665
|
+
const walPath = dbPath + '-wal';
|
|
666
|
+
const mtime = fs.existsSync(walPath) ? fs.statSync(walPath).mtimeMs : fs.statSync(dbPath).mtimeMs;
|
|
663
667
|
setOffset(dbPath + ':mtime', mtime);
|
|
664
668
|
}
|
|
665
|
-
//
|
|
669
|
+
// Extract to populate _cursorConvState with current bubble counts
|
|
666
670
|
const existing = extractCursorUsage(dbPath);
|
|
667
671
|
if (isFirstRun && existing.length > 0) {
|
|
668
|
-
log(`Cursor: marked ${
|
|
672
|
+
log(`Cursor: marked ${_cursorConvState.size} conversations as read (${existing.length} events skipped)`);
|
|
669
673
|
} else if (existing.length > 0) {
|
|
670
|
-
// Not first run but has new data since last mtime — report it
|
|
671
674
|
reportEvents(existing);
|
|
672
675
|
log(`Cursor: catch-up reported ${existing.length} events`);
|
|
673
676
|
}
|