@krtclcdy/tersio 2.13.0 → 2.14.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/README.md +1 -1
- package/cli/dashboard.js +2 -1
- package/cli/install.js +21 -12
- package/cli/reset.js +18 -13
- package/cli/usage.js +5 -4
- package/dashboard/app.js +27 -2
- package/dashboard/styles.css +10 -0
- package/dashboard/template.html +2 -6
- package/extensions/shared/rtk-gain.js +9 -3
- package/extensions/shared/usage-ledger.js +39 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn c
|
|
|
108
108
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
|
|
109
109
|
| `tersio usage` | Ledger-backed usage + savings report |
|
|
110
110
|
| `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`; serves localhost only) |
|
|
111
|
-
| `tersio reset` | Clear tersio statistics
|
|
111
|
+
| `tersio reset` | Clear tersio statistics: usage ledger + a watermark that hides pre-reset rows from every derived view (Y/N confirm, `--dry-run`, `--yes`) — session transcripts and RTK history stay intact on disk |
|
|
112
112
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary and its `rtk.ts` OMP wiring) |
|
|
113
113
|
| `tersio version` | Print version |
|
|
114
114
|
|
package/cli/dashboard.js
CHANGED
|
@@ -8,7 +8,7 @@ import { promises as fs } from 'node:fs';
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { clearUsageLedger, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
11
|
+
import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
12
12
|
import { summarizeUsage } from './usage.js';
|
|
13
13
|
import { withInteractiveSpinner } from './interactive.js';
|
|
14
14
|
const DASH_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard');
|
|
@@ -52,6 +52,7 @@ async function runDashboard(options) {
|
|
|
52
52
|
const server = http.createServer(async (req, res) => {
|
|
53
53
|
if (req.url === '/reset' && req.method === 'POST') {
|
|
54
54
|
const rows = clearUsageLedger();
|
|
55
|
+
markReset();
|
|
55
56
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
56
57
|
res.end(JSON.stringify({ ok: true, rows }));
|
|
57
58
|
return;
|
package/cli/install.js
CHANGED
|
@@ -444,13 +444,13 @@ function defaultProfile() {
|
|
|
444
444
|
}
|
|
445
445
|
// Install is always user-level (all OMP sessions). Project scope was removed:
|
|
446
446
|
// session extensions must live in ~/.omp/agent/extensions to load.
|
|
447
|
-
async function resolveProfile() {
|
|
447
|
+
async function resolveProfile(forceReinstall = false) {
|
|
448
448
|
const profile = defaultProfile();
|
|
449
449
|
// Single interactive prompt: the Combo preset implies all three modes.
|
|
450
450
|
// Numbered menu — no typing preset names.
|
|
451
451
|
// Only for a real user at a terminal, only when no default flags were
|
|
452
452
|
// given, and never for --apply-update runs.
|
|
453
|
-
if (tty() && !profileFlagsGiven && !applyUpdate && (install || reinstall)) {
|
|
453
|
+
if (tty() && !profileFlagsGiven && !applyUpdate && (install || forceReinstall || reinstall)) {
|
|
454
454
|
const choice = await askInteractiveChoice('Session-start defaults — Combo preset', [
|
|
455
455
|
{ value: 'off', label: 'off' },
|
|
456
456
|
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
@@ -558,11 +558,12 @@ async function runCommandMenu() {
|
|
|
558
558
|
updatePromptDone = true;
|
|
559
559
|
const choice = await askInteractiveChoice('Tersio — what next?', [
|
|
560
560
|
{ value: 'install', label: 'Install add-ons', hint: 'user scope + combo defaults' },
|
|
561
|
-
{ value: '
|
|
561
|
+
{ value: 'update', label: 'Update', hint: 'CLI version check, then refresh add-ons (RTK, Caveman rule, Ponytail)' },
|
|
562
|
+
{ value: 'reinstall', label: 'Reinstall', hint: 'clean and reinstall the add-ons, Ponytail package kept' },
|
|
562
563
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
563
564
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
564
565
|
{ value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
|
|
565
|
-
{ value: 'reset', label: 'Reset statistics', hint: 'clear
|
|
566
|
+
{ value: 'reset', label: 'Reset statistics', hint: 'clear statistics; transcripts and RTK history stay' },
|
|
566
567
|
{ value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
|
|
567
568
|
], 'install');
|
|
568
569
|
if (choice.status !== 'selected') {
|
|
@@ -573,8 +574,8 @@ async function runCommandMenu() {
|
|
|
573
574
|
case 'install':
|
|
574
575
|
await runInstall();
|
|
575
576
|
break;
|
|
576
|
-
case '
|
|
577
|
-
// One bound flow: report the CLI version, then offer the
|
|
577
|
+
case 'update': {
|
|
578
|
+
// One bound flow: report the CLI version, then offer the add-on
|
|
578
579
|
// refresh in the same pass — no redundant second "update" option.
|
|
579
580
|
const latest = await checkForUpdate(true);
|
|
580
581
|
if (typeof latest === 'string')
|
|
@@ -583,7 +584,7 @@ async function runCommandMenu() {
|
|
|
583
584
|
console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
|
|
584
585
|
else
|
|
585
586
|
console.log(` [ok] tersio ${PACKAGE_VERSION} is the latest`);
|
|
586
|
-
const go = await askInteractiveConfirm('Update
|
|
587
|
+
const go = await askInteractiveConfirm('Update add-ons now (RTK, Caveman rule, Ponytail)?');
|
|
587
588
|
if (go.status === 'confirmed' && go.value) {
|
|
588
589
|
await runLatestUpdate();
|
|
589
590
|
}
|
|
@@ -597,6 +598,10 @@ async function runCommandMenu() {
|
|
|
597
598
|
closeRL();
|
|
598
599
|
break;
|
|
599
600
|
}
|
|
601
|
+
case 'reinstall':
|
|
602
|
+
await runInstall({ reinstall: true });
|
|
603
|
+
closeRL();
|
|
604
|
+
break;
|
|
600
605
|
case 'doctor':
|
|
601
606
|
await runDoctor();
|
|
602
607
|
closeRL();
|
|
@@ -619,7 +624,8 @@ async function runCommandMenu() {
|
|
|
619
624
|
break;
|
|
620
625
|
}
|
|
621
626
|
}
|
|
622
|
-
async function runInstall() {
|
|
627
|
+
async function runInstall(overrides = {}) {
|
|
628
|
+
const isReinstall = overrides.reinstall ?? reinstall;
|
|
623
629
|
// Menu-driven installs must fall through: bare `tersio` re-enters here
|
|
624
630
|
// with command === null after the picker, and without this guard the
|
|
625
631
|
// choice loops straight back into runCommandMenu() forever.
|
|
@@ -628,8 +634,11 @@ async function runInstall() {
|
|
|
628
634
|
return;
|
|
629
635
|
}
|
|
630
636
|
printWelcome();
|
|
631
|
-
if (
|
|
632
|
-
|
|
637
|
+
if (isReinstall) {
|
|
638
|
+
// removeRtk stays false: reinstall is about to replace the binary, and
|
|
639
|
+
// deleting it first would leave nothing to wire if the fresh download
|
|
640
|
+
// fails (rate limit, offline). rtk.ts is removed here and re-wired below.
|
|
641
|
+
await runUninstall({ yes: true, removePonytail: false, removeRtk: false });
|
|
633
642
|
}
|
|
634
643
|
if (dryRun)
|
|
635
644
|
console.log('[dry-run] No changes will be written.\n');
|
|
@@ -665,13 +674,13 @@ async function runInstall() {
|
|
|
665
674
|
}
|
|
666
675
|
console.log(' Scope: user (all OMP sessions)');
|
|
667
676
|
// Resolve session defaults: flags > interactive prompt > defaults.
|
|
668
|
-
const profile = await resolveProfile();
|
|
677
|
+
const profile = await resolveProfile(isReinstall);
|
|
669
678
|
const userDir = OMP_AGENT_DIR;
|
|
670
679
|
const userExtDir = path.join(userDir, 'extensions');
|
|
671
680
|
// apply-update is `tersio update`'s payload run: treat it like reinstall so
|
|
672
681
|
// the add-ons refresh too — Ponytail package via npm, self plugin, RTK
|
|
673
682
|
// binary (always re-downloaded), and the Caveman rule (always re-fetched).
|
|
674
|
-
const options = { dryRun, verbose, yes, reinstall:
|
|
683
|
+
const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate };
|
|
675
684
|
// Check prerequisites
|
|
676
685
|
console.log('\nPrerequisites:');
|
|
677
686
|
try {
|
package/cli/reset.js
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
// cli/reset.ts — clear tersio-owned statistics
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// cli/reset.ts — clear tersio-owned statistics: the usage ledger plus a reset
|
|
2
|
+
// watermark that filters session-derived and RTK-metered rows out of every
|
|
3
|
+
// derived view. Session transcripts (~/.omp/agent/sessions) and the RTK
|
|
4
|
+
// database are host/tool-owned and are never touched; the watermark only
|
|
5
|
+
// changes what tersio shows.
|
|
4
6
|
import { cancel as clackCancel, confirm as clackConfirm } from '@clack/prompts';
|
|
5
7
|
import { dryRun, yes } from './common.js';
|
|
6
8
|
import { ask, closeRL, tty } from './interactive.js';
|
|
7
|
-
import { clearUsageLedger, ledgerPath, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
8
|
-
import {
|
|
9
|
-
import { rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
9
|
+
import { clearUsageLedger, importSessionTokens, ledgerPath, markReset, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
10
|
+
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
10
11
|
async function runReset() {
|
|
11
12
|
console.log('\n=== Tersio Reset ===\n');
|
|
12
13
|
const rows = readUsage().length;
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const sessions = importSessionTokens();
|
|
15
|
+
const rtk = readRtkGain(1);
|
|
16
|
+
console.log(`Will clear: ${rows} usage ledger rows · ${ledgerPath()}`);
|
|
17
|
+
console.log(`Will hide (view-level watermark): session-derived statistics (${sessions.messages} messages) · RTK metered rows (${rtk.commands})`);
|
|
18
|
+
console.log(`Left intact on disk: sessions ${sessionsDir()}, rtk ${rtkDbPath()}`);
|
|
15
19
|
if (dryRun) {
|
|
16
|
-
console.log('[dry-run]
|
|
20
|
+
console.log('[dry-run] nothing written.');
|
|
17
21
|
return true;
|
|
18
22
|
}
|
|
19
|
-
if (rows === 0) {
|
|
20
|
-
console.log('Nothing to reset —
|
|
23
|
+
if (rows === 0 && sessions.messages === 0 && rtk.commands === 0) {
|
|
24
|
+
console.log('Nothing to reset — no statistics recorded.');
|
|
21
25
|
return true;
|
|
22
26
|
}
|
|
23
27
|
if (!yes) {
|
|
24
28
|
if (tty()) {
|
|
25
29
|
closeRL();
|
|
26
|
-
const confirmedChoice = await clackConfirm({ message: 'Clear
|
|
30
|
+
const confirmedChoice = await clackConfirm({ message: 'Clear tersio statistics?', initialValue: false });
|
|
27
31
|
if (typeof confirmedChoice !== 'boolean') {
|
|
28
32
|
clackCancel('Aborted.');
|
|
29
33
|
closeRL();
|
|
@@ -45,7 +49,8 @@ async function runReset() {
|
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
const cleared = clearUsageLedger();
|
|
48
|
-
|
|
52
|
+
const ts = markReset();
|
|
53
|
+
console.log(`[ok] reset — removed ${cleared} usage rows; statistics view starts at ${new Date(ts).toISOString()}`);
|
|
49
54
|
return true;
|
|
50
55
|
}
|
|
51
56
|
export { runReset };
|
package/cli/usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
-
import { co2GramsFor, energyWhFor, importSessionTokens, ledgerPath, priceFor, readUsage, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
|
-
import { readRtkGain
|
|
2
|
+
import { co2GramsFor, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readUsage, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
|
+
import { readRtkGain } from '../extensions/shared/rtk-gain.js';
|
|
4
4
|
import { withInteractiveSpinner } from './interactive.js';
|
|
5
5
|
import { PACKAGE_VERSION } from './common.js';
|
|
6
6
|
export function summarizeUsage(rows) {
|
|
@@ -12,6 +12,7 @@ export function summarizeUsage(rows) {
|
|
|
12
12
|
}
|
|
13
13
|
const byDetail = Object.entries(detailCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
14
14
|
const session = importSessionTokens();
|
|
15
|
+
const watermark = readResetWatermark();
|
|
15
16
|
let usd = 0;
|
|
16
17
|
let priced = true;
|
|
17
18
|
let savedUsd = 0;
|
|
@@ -44,7 +45,7 @@ export function summarizeUsage(rows) {
|
|
|
44
45
|
byDayModel: session.byDayModel,
|
|
45
46
|
byTool,
|
|
46
47
|
recent: session.recent,
|
|
47
|
-
rtkGain: readRtkGain(),
|
|
48
|
+
rtkGain: readRtkGain(10, watermark || undefined),
|
|
48
49
|
usd,
|
|
49
50
|
priced,
|
|
50
51
|
savedUsd,
|
|
@@ -52,7 +53,7 @@ export function summarizeUsage(rows) {
|
|
|
52
53
|
co2g,
|
|
53
54
|
energyWh,
|
|
54
55
|
version: PACKAGE_VERSION,
|
|
55
|
-
paths: { ledger: ledgerPath(), sessions: sessionsDir()
|
|
56
|
+
paths: { ledger: ledgerPath(), sessions: sessionsDir() },
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
function fmt(n) {
|
package/dashboard/app.js
CHANGED
|
@@ -778,7 +778,7 @@
|
|
|
778
778
|
renderSpark(byDay, d.usd || 0, d.savedUsd || 0, total);
|
|
779
779
|
document.getElementById('verChip').textContent = 'tersio v' + (d.version || '?');
|
|
780
780
|
var dp = d.paths || {};
|
|
781
|
-
[['pathLedger', dp.ledger], ['pathSessions', dp.sessions]
|
|
781
|
+
[['pathLedger', dp.ledger], ['pathSessions', dp.sessions]].forEach(function(pair) {
|
|
782
782
|
var pel = document.getElementById(pair[0]);
|
|
783
783
|
if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
|
|
784
784
|
});
|
|
@@ -887,6 +887,27 @@
|
|
|
887
887
|
observe();
|
|
888
888
|
});
|
|
889
889
|
}
|
|
890
|
+
function toast(title, desc, icon) {
|
|
891
|
+
var wrap = document.querySelector('.toaster');
|
|
892
|
+
if (!wrap) {
|
|
893
|
+
wrap = document.createElement('div');
|
|
894
|
+
wrap.className = 'toaster';
|
|
895
|
+
wrap.popover = 'manual';
|
|
896
|
+
document.body.appendChild(wrap);
|
|
897
|
+
if (wrap.showPopover) wrap.showPopover(); // top layer: paints above the native settings dialog
|
|
898
|
+
}
|
|
899
|
+
var el = document.createElement('div');
|
|
900
|
+
el.className = 'toast';
|
|
901
|
+
el.innerHTML = '<span class="toast-icon"><i data-lucide="' + (icon || 'check') + '" class="size-4"></i></span><div><p class="toast-title"></p><p class="toast-desc"></p></div>';
|
|
902
|
+
el.querySelector('.toast-title').textContent = title;
|
|
903
|
+
el.querySelector('.toast-desc').textContent = desc;
|
|
904
|
+
wrap.appendChild(el);
|
|
905
|
+
if (window.lucide) lucide.createIcons();
|
|
906
|
+
setTimeout(function() {
|
|
907
|
+
el.classList.add('out');
|
|
908
|
+
setTimeout(function() { el.remove(); }, 200);
|
|
909
|
+
}, 4000);
|
|
910
|
+
}
|
|
890
911
|
document.getElementById('reload').addEventListener('click', load);
|
|
891
912
|
(function() {
|
|
892
913
|
var dlg = document.getElementById('settings');
|
|
@@ -909,7 +930,11 @@
|
|
|
909
930
|
fetch('reset', { method: 'POST' }).then(function(r) { return r.json(); }).then(function() {
|
|
910
931
|
label.textContent = 'Reset';
|
|
911
932
|
load();
|
|
912
|
-
|
|
933
|
+
toast('Statistics reset', 'The usage statistics view now starts fresh. Transcripts and RTK history were never touched.', 'rotate-ccw');
|
|
934
|
+
}).catch(function() {
|
|
935
|
+
label.textContent = 'Reset';
|
|
936
|
+
toast('Reset failed', 'Could not reach the server. Try again.', 'circle-alert');
|
|
937
|
+
});
|
|
913
938
|
} else {
|
|
914
939
|
resetBtn.dataset.armed = '1';
|
|
915
940
|
label.textContent = 'Sure?';
|
package/dashboard/styles.css
CHANGED
|
@@ -201,3 +201,13 @@
|
|
|
201
201
|
#reset { color: var(--danger); }
|
|
202
202
|
#reset:hover { border-color: var(--danger); box-shadow: 0 0 14px var(--danger-border); }
|
|
203
203
|
.dlg .seg button { display: grid; place-items: center; }
|
|
204
|
+
|
|
205
|
+
/* Toaster (shadcn/ui sonner-style, dependency-free) */
|
|
206
|
+
.toaster { position: fixed; inset: auto 16px 16px auto; z-index: 100; display: flex; flex-direction: column; gap: 8px; width: min(360px, calc(100vw - 32px)); border: 0; padding: 0; background: transparent; overflow: visible; }
|
|
207
|
+
.toast { display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px; border: 1px solid var(--line); background: var(--panel); border-radius: 12px; box-shadow: var(--shadow-xs), 0 8px 24px rgba(0, 0, 0, .22); animation: toast-in .18s ease-out; }
|
|
208
|
+
.toast.out { animation: toast-out .18s ease-in forwards; }
|
|
209
|
+
.toast-icon { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; background: var(--accent-soft); color: var(--accent); flex: none; }
|
|
210
|
+
.toast-title { font-size: 13px; font-weight: 600; margin: 0; }
|
|
211
|
+
.toast-desc { font-size: 12px; color: var(--dim); margin: 1px 0 0; }
|
|
212
|
+
@keyframes toast-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }
|
|
213
|
+
@keyframes toast-out { to { opacity: 0; transform: translateY(6px) scale(.98); } }
|
package/dashboard/template.html
CHANGED
|
@@ -83,17 +83,13 @@
|
|
|
83
83
|
<p class="dlg-name">Session transcripts <span class="dlg-own">host-owned · never touched</span></p>
|
|
84
84
|
<p class="mono dlg-file" id="pathSessions" title="">–</p>
|
|
85
85
|
</div>
|
|
86
|
-
<div class="dlg-path">
|
|
87
|
-
<p class="dlg-name">RTK history <span class="dlg-own">rtk-owned · never touched</span></p>
|
|
88
|
-
<p class="mono dlg-file" id="pathRtk" title="">–</p>
|
|
89
|
-
</div>
|
|
90
86
|
</div>
|
|
91
87
|
<div class="dlg-danger">
|
|
92
88
|
<p class="dlg-label">Danger zone</p>
|
|
93
89
|
<div class="dlg-row">
|
|
94
90
|
<div class="min-w-0">
|
|
95
91
|
<p class="dlg-name">Reset statistics</p>
|
|
96
|
-
<p class="dlg-hint">Clears
|
|
92
|
+
<p class="dlg-hint">Clears tersio statistics. Host-owned files stay intact. Click twice to confirm.</p>
|
|
97
93
|
</div>
|
|
98
94
|
<button id="reset" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--danger-border)" aria-label="Reset tersio statistics">
|
|
99
95
|
<i data-lucide="rotate-ccw" class="size-3.5"></i><span id="resetLabel">Reset</span>
|
|
@@ -292,7 +288,7 @@
|
|
|
292
288
|
</span>
|
|
293
289
|
<span class="ml-auto flex items-center gap-1" id="cmdPages" role="navigation" aria-label="Pages"></span>
|
|
294
290
|
</div>
|
|
295
|
-
<p class="mono text-[11px] mt-3" style="color: var(--dim)">
|
|
291
|
+
<p class="mono text-[11px] mt-3" style="color: var(--dim)">bash commands run through the rtk-wired OMP hook meter automatically; rows without metering show –. caveman + ponytail gains are instruction-following, bench-measured in BENCHMARK.md.</p>
|
|
296
292
|
</section>
|
|
297
293
|
|
|
298
294
|
<footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
@@ -33,12 +33,18 @@ function query(db, sql) {
|
|
|
33
33
|
.filter((line) => line.trim())
|
|
34
34
|
.map((line) => line.split('\t'));
|
|
35
35
|
}
|
|
36
|
-
export function readRtkGain(limit = 10) {
|
|
36
|
+
export function readRtkGain(limit = 10, cutoffMs) {
|
|
37
37
|
try {
|
|
38
38
|
if (!fs.existsSync(rtkDbPath()))
|
|
39
39
|
return EMPTY;
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// View-level cutoff only: rows stay in rtk's own database untouched.
|
|
41
|
+
// strftime('%s') normalizes ISO-8601 timestamps to epoch seconds; rows
|
|
42
|
+
// with unparseable timestamps (0) drop out of a filtered view.
|
|
43
|
+
const where = cutoffMs && cutoffMs > 0
|
|
44
|
+
? `WHERE CAST(strftime('%s', timestamp) AS INTEGER) >= ${Math.floor(cutoffMs / 1000)}`
|
|
45
|
+
: '';
|
|
46
|
+
const [[commands = '0', saved = '0', input = '0', avgPct = '0', totalMs = '0'] = []] = query(rtkDbPath(), `SELECT COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(SUM(input_tokens),0), COALESCE(AVG(savings_pct),0), COALESCE(SUM(exec_time_ms),0) FROM commands ${where};`);
|
|
47
|
+
const byCommand = query(rtkDbPath(), `SELECT rtk_cmd, COUNT(*), SUM(saved_tokens), AVG(savings_pct), AVG(exec_time_ms) FROM commands ${where} GROUP BY rtk_cmd ORDER BY SUM(saved_tokens) DESC LIMIT ${Math.max(1, Math.floor(limit))};`).map(([command, count, savedRow, pct, ms]) => ({
|
|
42
48
|
command,
|
|
43
49
|
count: Number(count) || 0,
|
|
44
50
|
saved: Number(savedRow) || 0,
|
|
@@ -11,6 +11,34 @@ export function ledgerPath() {
|
|
|
11
11
|
return override;
|
|
12
12
|
return path.join(os.homedir(), '.omp', 'plugins', 'tersio-usage.jsonl');
|
|
13
13
|
}
|
|
14
|
+
// Reset watermark: tersio-owned timestamp marking the last statistics reset.
|
|
15
|
+
// Session transcripts and the RTK database are host/tool-owned and never
|
|
16
|
+
// touched — instead, every derived view (token stats, command tools) filters
|
|
17
|
+
// rows from before the watermark, so "reset" empties what tersio shows
|
|
18
|
+
// without deleting anything it does not own.
|
|
19
|
+
export function resetMarkerPath() {
|
|
20
|
+
const override = process.env.TERSIO_RESET_FILE;
|
|
21
|
+
if (override)
|
|
22
|
+
return override;
|
|
23
|
+
return path.join(os.homedir(), '.omp', 'plugins', 'tersio-reset.json');
|
|
24
|
+
}
|
|
25
|
+
export function readResetWatermark() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(fs.readFileSync(resetMarkerPath(), 'utf8'));
|
|
28
|
+
return typeof raw.ts === 'number' && Number.isFinite(raw.ts) && raw.ts > 0 ? raw.ts : 0;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function markReset(now = Date.now()) {
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(path.dirname(resetMarkerPath()), { recursive: true });
|
|
37
|
+
fs.writeFileSync(resetMarkerPath(), JSON.stringify({ ts: now }) + '\n', 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch { /* best-effort; never break the caller */ }
|
|
40
|
+
return now;
|
|
41
|
+
}
|
|
14
42
|
export function appendUsage(kind, detail) {
|
|
15
43
|
const row = JSON.stringify({ ts: Date.now(), kind, detail }) + '\n';
|
|
16
44
|
try {
|
|
@@ -71,6 +99,7 @@ function walkJsonl(dir, out, cap) {
|
|
|
71
99
|
}
|
|
72
100
|
}
|
|
73
101
|
export function importSessionTokens() {
|
|
102
|
+
const watermark = readResetWatermark();
|
|
74
103
|
const byModel = {};
|
|
75
104
|
const byDay = {};
|
|
76
105
|
const byDayModel = {};
|
|
@@ -88,13 +117,17 @@ export function importSessionTokens() {
|
|
|
88
117
|
}
|
|
89
118
|
let codexProvider = null;
|
|
90
119
|
const recent = [];
|
|
120
|
+
// Rows from before the reset watermark (and rows with no usable timestamp,
|
|
121
|
+
// which may predate it) stay out of every derived statistic.
|
|
91
122
|
function ingest(model, usage, ts) {
|
|
123
|
+
const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
|
|
124
|
+
if (watermark > 0 && (!Number.isFinite(ms) || ms < watermark))
|
|
125
|
+
return false;
|
|
92
126
|
addInto(totals, usage);
|
|
93
127
|
byModel[model] ??= zeroBreakdown();
|
|
94
128
|
addInto(byModel[model], usage);
|
|
95
129
|
byModelMessages[model] = (byModelMessages[model] ?? 0) + 1;
|
|
96
130
|
const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
97
|
-
const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
|
|
98
131
|
if (Number.isFinite(ms))
|
|
99
132
|
recent.push({ m: model, i: num(usage.input), o: num(usage.output), t: ms });
|
|
100
133
|
const day = ts !== undefined ? dayKey(ts) : null;
|
|
@@ -108,6 +141,7 @@ export function importSessionTokens() {
|
|
|
108
141
|
}
|
|
109
142
|
}
|
|
110
143
|
messages += 1;
|
|
144
|
+
return true;
|
|
111
145
|
}
|
|
112
146
|
for (const file of files) {
|
|
113
147
|
let text;
|
|
@@ -145,9 +179,11 @@ export function importSessionTokens() {
|
|
|
145
179
|
if (!msg || msg.role !== 'assistant' || !msg.usage)
|
|
146
180
|
continue;
|
|
147
181
|
const model = typeof msg.model === 'string' && msg.model ? msg.model : 'unknown';
|
|
148
|
-
ingest(model, msg.usage, row.timestamp);
|
|
149
|
-
if (typeof msg.usage.cost === 'number' && Number.isFinite(msg.usage.cost))
|
|
182
|
+
const counted = ingest(model, msg.usage, row.timestamp);
|
|
183
|
+
if (counted && typeof msg.usage.cost === 'number' && Number.isFinite(msg.usage.cost))
|
|
150
184
|
costMeasured += msg.usage.cost;
|
|
185
|
+
if (!counted)
|
|
186
|
+
continue;
|
|
151
187
|
for (const part of msg.content ?? []) {
|
|
152
188
|
if (part?.type === 'toolCall' && typeof part.name === 'string' && part.name) {
|
|
153
189
|
const key = part.name === 'bash' ? `bash:${leadBinary(part.arguments?.command)}` : part.name;
|
package/package.json
CHANGED