@krtclcdy/tersio 2.7.0 → 2.8.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 +2 -2
- package/cli/common.js +4 -4
- package/cli/dashboard.html +387 -211
- package/cli/dashboard.js +3 -3
- package/cli/install.js +2 -7
- package/cli/usage.js +15 -3
- package/extensions/shared/usage-ledger.js +35 -1
- package/extensions/tersio-commands/index.js +2 -2
- package/package.json +1 -1
- package/tersio.js +6 -6
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead
|
|
|
82
82
|
| `tersio reinstall` | Fresh install, preserving the Ponytail package |
|
|
83
83
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
|
|
84
84
|
| `tersio usage` | Ledger-backed usage + savings report |
|
|
85
|
-
| `tersio
|
|
85
|
+
| `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`; serves localhost only) |
|
|
86
86
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
|
|
87
87
|
| `tersio version` | Print version |
|
|
88
88
|
|
|
@@ -101,7 +101,7 @@ Everything runs under one root. Bare `/tersio` prints status.
|
|
|
101
101
|
| `/tersio status` | Active modes + combo level; state persists, propagates to subagents, and lights the Combo footer indicator. |
|
|
102
102
|
| `/tersio check` | Add-on version check |
|
|
103
103
|
| `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | Update add-ons, preview with dry-run |
|
|
104
|
-
| `/tersio gain` | Savings summary; the gain dashboard charts `~/.omp/plugins/tersio-usage.jsonl` (`tersio
|
|
104
|
+
| `/tersio gain` | Savings summary; the gain dashboard charts `~/.omp/plugins/tersio-usage.jsonl` (`tersio gain --open` serves on 127.0.0.1, `--export` writes one file). |
|
|
105
105
|
| `/tersio usage` | Ledger report for this machine (rows in `~/.omp/plugins/tersio-usage.jsonl`, local only) |
|
|
106
106
|
| `/tersio help` | This table |
|
|
107
107
|
| `/caveman`, `/rtk`, `/combo`, `/ai-addons` | Legacy aliases, still work; prefer `/tersio`. |
|
package/cli/common.js
CHANGED
|
@@ -46,11 +46,11 @@ function flagValue(name) {
|
|
|
46
46
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
47
47
|
}
|
|
48
48
|
// --- CLI flags ---
|
|
49
|
-
const COMMANDS =
|
|
49
|
+
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, version: true, help: true };
|
|
50
50
|
const args = process.argv.slice(2);
|
|
51
51
|
const commandArg = args.find((arg, index) => !arg.startsWith('-') && args[index - 1] !== '--scope');
|
|
52
52
|
const command = commandArg?.toLowerCase() || null;
|
|
53
|
-
const unknownCommand = command !== null && !COMMANDS
|
|
53
|
+
const unknownCommand = command !== null && !COMMANDS[command];
|
|
54
54
|
const install = command === 'install';
|
|
55
55
|
const update = command === 'update';
|
|
56
56
|
const reinstall = command === 'reinstall';
|
|
@@ -62,7 +62,7 @@ const yes = args.includes('--yes') || args.includes('-y') || install || update |
|
|
|
62
62
|
const verbose = args.includes('--verbose');
|
|
63
63
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
64
64
|
const usage = command === 'usage' || args.includes('--usage');
|
|
65
|
-
const
|
|
65
|
+
const gain = command === 'gain';
|
|
66
66
|
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
67
67
|
const dashboardOpen = args.includes('--open');
|
|
68
68
|
const dashboardExport = flagValue('--export') ?? null;
|
|
@@ -254,4 +254,4 @@ function relTime(ageMs) {
|
|
|
254
254
|
const weeks = Math.floor(days / 7);
|
|
255
255
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
256
256
|
}
|
|
257
|
-
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage,
|
|
257
|
+
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage, gain, dashboardPort, dashboardOpen, dashboardExport, removePonytail, keepPonytail, removeRtk, scopeFlag, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, SCOPE_MAP, relTime, };
|
package/cli/dashboard.html
CHANGED
|
@@ -8,26 +8,24 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" href="brand.webp">
|
|
9
9
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
10
10
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
|
|
13
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/outfit@5/index.css">
|
|
11
14
|
<style>
|
|
12
15
|
:root {
|
|
13
16
|
--bg: #f4f4f5; --panel: #ffffff; --ink: #18181b; --dim: #52525b;
|
|
14
17
|
--line: rgba(24, 24, 27, .12); --accent: #047857; --accent-soft: rgba(4, 120, 87, .12);
|
|
15
18
|
--track: rgba(24, 24, 27, .08); --grid: rgba(4, 120, 87, .07);
|
|
16
|
-
--cell-0: rgba(24, 24, 27, .06);
|
|
17
|
-
--cell-1: rgba(4, 120, 87, .25); --cell-2: rgba(4, 120, 87, .5);
|
|
18
|
-
--cell-3: rgba(4, 120, 87, .75); --cell-4: #047857;
|
|
19
19
|
}
|
|
20
20
|
@media (prefers-color-scheme: dark) {
|
|
21
21
|
:root {
|
|
22
22
|
--bg: #09090b; --panel: #101014; --ink: #f4f4f5; --dim: #a1a1aa;
|
|
23
23
|
--line: rgba(255, 255, 255, .09); --accent: #34d399; --accent-soft: rgba(52, 211, 153, .1);
|
|
24
24
|
--track: rgba(255, 255, 255, .07); --grid: rgba(52, 211, 153, .05);
|
|
25
|
-
--cell-0: rgba(255, 255, 255, .06);
|
|
26
|
-
--cell-1: rgba(52, 211, 153, .25); --cell-2: rgba(52, 211, 153, .5);
|
|
27
|
-
--cell-3: rgba(52, 211, 153, .75); --cell-4: #34d399;
|
|
28
25
|
}
|
|
29
26
|
}
|
|
30
27
|
body { background: var(--bg); color: var(--ink); }
|
|
28
|
+
.display { font-family: "Outfit", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
31
29
|
.bg-blueprint {
|
|
32
30
|
background-image: linear-gradient(var(--grid) 1px, transparent 1px),
|
|
33
31
|
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
|
@@ -38,15 +36,10 @@
|
|
|
38
36
|
.rowline { border-bottom: 1px solid var(--line); }
|
|
39
37
|
.bar-fill { background: var(--accent); border-radius: 999px; transform-origin: left; }
|
|
40
38
|
.bar-track { background: var(--track); border-radius: 999px; }
|
|
41
|
-
.bar-dim { background: var(--dim); opacity: .55; border-radius: 999px; transform-origin: bottom; }
|
|
42
39
|
.live-dot { background: var(--accent); box-shadow: 0 0 0 0 var(--accent-soft); animation: ping 2s cubic-bezier(0, 0, .2, 1) infinite; }
|
|
43
40
|
@keyframes ping { 75%, 100% { box-shadow: 0 0 0 9px transparent; } }
|
|
44
41
|
.rise { opacity: 0; transform: translateY(26px); }
|
|
45
42
|
.rise.in { opacity: 1; transform: none; transition: opacity .7s cubic-bezier(.16, 1, .3, 1), transform .7s cubic-bezier(.16, 1, .3, 1); }
|
|
46
|
-
.grow { transform: scaleX(0); transition: transform 1.1s cubic-bezier(.16, 1, .3, 1); }
|
|
47
|
-
.grow.in { transform: scaleX(1); }
|
|
48
|
-
.growY { transform: scaleY(0); transition: transform .9s cubic-bezier(.16, 1, .3, 1); }
|
|
49
|
-
.growY.in { transform: scaleY(1); }
|
|
50
43
|
.tilt { transition: transform .25s cubic-bezier(.16, 1, .3, 1), background .25s; }
|
|
51
44
|
.tilt:hover { transform: translateX(6px); background: var(--accent-soft); }
|
|
52
45
|
.btn-push { transition: transform .12s, background .2s; }
|
|
@@ -54,9 +47,12 @@
|
|
|
54
47
|
.btn-push:active { transform: scale(.96); }
|
|
55
48
|
a.social { color: var(--ink); }
|
|
56
49
|
a.social:hover { color: var(--accent); }
|
|
57
|
-
.
|
|
58
|
-
.
|
|
59
|
-
.
|
|
50
|
+
.seg button { color: var(--dim); border-radius: 8px; transition: background .2s, color .2s; }
|
|
51
|
+
.seg button.on { background: var(--accent-soft); color: var(--ink); }
|
|
52
|
+
.seg button:not(.on):hover { color: var(--ink); }
|
|
53
|
+
.mbar { display: flex; flex-direction: column-reverse; border-radius: 6px 6px 2px 2px; overflow: hidden; min-height: 3px; background: var(--track); }
|
|
54
|
+
.mbar > span { display: block; width: 100%; }
|
|
55
|
+
.monogram { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; font-weight: 700; font-size: 13px; color: #fff; flex-shrink: 0; }
|
|
60
56
|
.dock {
|
|
61
57
|
position: sticky; top: 12px; z-index: 50;
|
|
62
58
|
background: color-mix(in srgb, var(--panel) 84%, transparent);
|
|
@@ -64,25 +60,18 @@
|
|
|
64
60
|
border: 1px solid var(--line); border-radius: 20px;
|
|
65
61
|
box-shadow: 0 12px 40px rgba(0, 0, 0, .25);
|
|
66
62
|
}
|
|
67
|
-
.dock a.docklink { transition: transform .22s cubic-bezier(.34, 1.56, .64, 1), background .2s; border-radius: 12px; }
|
|
68
|
-
.dock a.docklink:hover { transform: scale(1.28) translateY(-3px); background: var(--accent-soft); }
|
|
69
|
-
.dock a.docklink:active { transform: scale(1.05); }
|
|
70
63
|
.dock img.brand { border-radius: 10px; }
|
|
71
|
-
@media (prefers-reduced-motion: reduce) {
|
|
72
|
-
.dock a.docklink { transition: none; }
|
|
73
|
-
.dock a.docklink:hover { transform: none; }
|
|
74
|
-
}
|
|
75
64
|
@media (prefers-reduced-motion: reduce) {
|
|
76
65
|
.rise { opacity: 1; transform: none; }
|
|
77
|
-
.grow, .growY { transform: none; transition: none; }
|
|
78
66
|
.live-dot { animation: none; }
|
|
79
|
-
.tilt, .btn-push { transition: none; }
|
|
67
|
+
.tilt, .btn-push, .seg button { transition: none; }
|
|
80
68
|
}
|
|
81
69
|
</style>
|
|
82
70
|
</head>
|
|
83
71
|
<body class="min-h-[100dvh] antialiased">
|
|
84
72
|
<div class="bg-blueprint pointer-events-none fixed inset-0" aria-hidden="true"></div>
|
|
85
73
|
|
|
74
|
+
<main class="overflow-x-hidden w-full max-w-full">
|
|
86
75
|
<div class="relative mx-auto max-w-7xl px-4 sm:px-6 pb-16">
|
|
87
76
|
|
|
88
77
|
<!-- Floating dock navbar -->
|
|
@@ -98,94 +87,99 @@
|
|
|
98
87
|
<span class="live-dot inline-block size-2 rounded-full"></span>
|
|
99
88
|
<span id="clock">--:--</span>
|
|
100
89
|
</span>
|
|
101
|
-
<button id="reload" 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(--line)" aria-label="Reload data">
|
|
90
|
+
<button id="reload" 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(--line); color: var(--ink)" aria-label="Reload data">
|
|
102
91
|
<i data-lucide="refresh-cw" class="size-3.5"></i><span class="hidden md:inline">Reload</span>
|
|
103
92
|
</button>
|
|
104
93
|
</header>
|
|
105
94
|
|
|
106
|
-
<!-- Hero:
|
|
107
|
-
<section class="
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
<!-- Hero: centered total -->
|
|
96
|
+
<section class="mx-auto max-w-6xl text-center pt-14 pb-10">
|
|
97
|
+
<p class="hero-in mono text-[11px] uppercase tracking-[0.22em] mb-4" style="color: var(--accent)">Live token feed</p>
|
|
98
|
+
<p class="hero-in mono font-bold tracking-tighter leading-none text-6xl md:text-7xl" id="total">0</p>
|
|
99
|
+
<p class="hero-in mono mt-4 text-sm max-w-[62ch] mx-auto" style="color: var(--dim)" id="herosub">session tokens parsed from OMP transcripts. Input, output, cache read, cache write, USD cost, CO2.</p>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<!-- Savings bento: 3+3+2+2+2 -->
|
|
103
|
+
<section class="grid grid-cols-12 grid-flow-dense gap-3" aria-label="Savings">
|
|
104
|
+
<div class="rise col-span-6 md:col-span-3 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
|
|
105
|
+
<p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">usd cost</p>
|
|
106
|
+
<p class="mono font-bold text-3xl mt-1" id="usd">-</p>
|
|
107
|
+
<p class="mono text-[11px] mt-1" style="color: var(--dim)" id="usdNote">per-model price table</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="rise col-span-6 md:col-span-3 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
|
|
110
|
+
<p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">saved by cache</p>
|
|
111
|
+
<p class="mono font-bold text-3xl mt-1" style="color: var(--accent)" id="saved">-</p>
|
|
112
|
+
<p class="mono text-[11px] mt-1" style="color: var(--dim)">est. vs full input price</p>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="rise col-span-4 md:col-span-2 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
|
|
115
|
+
<p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">co2</p>
|
|
116
|
+
<p class="mono font-bold text-3xl mt-1" id="co2">-</p>
|
|
117
|
+
<p class="mono text-[11px] mt-1" style="color: var(--dim)">est. from output</p>
|
|
112
118
|
</div>
|
|
113
|
-
<div class="
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
<div class="rise col-span-4 md:col-span-2 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
|
|
120
|
+
<p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">cache share</p>
|
|
121
|
+
<p class="mono font-bold text-3xl mt-1" id="cacheShare">-</p>
|
|
122
|
+
<p class="mono text-[11px] mt-1" style="color: var(--dim)">of tokens cached</p>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="rise col-span-4 md:col-span-2 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
|
|
125
|
+
<p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">assistant msgs</p>
|
|
126
|
+
<p class="mono font-bold text-3xl mt-1" id="msgs">0</p>
|
|
127
|
+
<p class="mono text-[11px] mt-1" style="color: var(--dim)" id="msgNote">sessions parsed</p>
|
|
119
128
|
</div>
|
|
120
129
|
</section>
|
|
121
130
|
|
|
122
|
-
<!-- Token strip:
|
|
123
|
-
<section class="rise
|
|
131
|
+
<!-- Token strip: nonzero buckets only -->
|
|
132
|
+
<section class="rise mt-3 flex rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" id="tokens" aria-label="Tokens by bucket"></section>
|
|
124
133
|
|
|
125
|
-
<!--
|
|
126
|
-
<section id="activity" class="rise mt-8 rounded-xl p-5
|
|
127
|
-
<div class="flex items-center gap-
|
|
128
|
-
<i data-lucide="
|
|
129
|
-
<h2 class="font-bold tracking-tight text-xl">
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</div>
|
|
136
|
-
<div class="mono text-[11px] mt-3 flex items-center gap-1.5" style="color: var(--dim)">
|
|
137
|
-
<span>less</span>
|
|
138
|
-
<span class="cell"></span><span class="cell l1"></span><span class="cell l2"></span><span class="cell l3"></span><span class="cell l4"></span>
|
|
139
|
-
<span>more</span>
|
|
134
|
+
<!-- Token activity: stacked per-model bars + range toggle -->
|
|
135
|
+
<section id="activity" class="rise mt-8 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Token activity">
|
|
136
|
+
<div class="flex flex-wrap items-center gap-3 mb-1">
|
|
137
|
+
<i data-lucide="chart-column" class="size-4" style="color: var(--accent)"></i>
|
|
138
|
+
<h2 class="display font-bold tracking-tight text-xl">Token activity</h2>
|
|
139
|
+
<div class="seg mono text-xs flex items-center gap-1 ml-auto p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="tablist" aria-label="Range">
|
|
140
|
+
<button data-range="daily" class="on px-3 py-1.5" role="tab">Daily</button>
|
|
141
|
+
<button data-range="weekly" class="px-3 py-1.5" role="tab">Weekly</button>
|
|
142
|
+
<button data-range="cumulative" class="px-3 py-1.5" role="tab">Cumulative</button>
|
|
143
|
+
</div>
|
|
140
144
|
</div>
|
|
145
|
+
<p class="mono text-xs mb-4" style="color: var(--dim)" id="rangeNote">stacked by model / last 60 days</p>
|
|
146
|
+
<div class="flex items-end gap-[3px] h-56" id="bars"></div>
|
|
147
|
+
<div class="flex gap-[3px] mt-2 mono text-[10px]" style="color: var(--dim)" id="barDays"></div>
|
|
148
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1.5 mt-4 mono text-[11px]" style="color: var(--dim)" id="legend"></div>
|
|
141
149
|
</section>
|
|
142
150
|
|
|
143
|
-
<!--
|
|
144
|
-
<
|
|
151
|
+
<!-- Top models: 3 cards -->
|
|
152
|
+
<section class="mt-8" aria-label="Top models">
|
|
153
|
+
<div class="flex items-center gap-2 mb-1">
|
|
154
|
+
<i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
|
|
155
|
+
<h2 class="display font-bold tracking-tight text-xl">Top models</h2>
|
|
156
|
+
<span class="mono text-xs ml-auto" style="color: var(--dim)">usage across sessions</span>
|
|
157
|
+
</div>
|
|
158
|
+
<p class="mono text-xs mb-4" style="color: var(--dim)">week-over-week vs prior 7 days</p>
|
|
159
|
+
<div class="grid grid-cols-1 md:grid-cols-12 grid-flow-dense gap-3" id="modelCards"></div>
|
|
160
|
+
</section>
|
|
145
161
|
|
|
146
|
-
|
|
147
|
-
|
|
162
|
+
<!-- Models list + top tools: 8 + 4 -->
|
|
163
|
+
<main class="grid grid-cols-1 lg:grid-cols-12 grid-flow-dense gap-8 mt-8">
|
|
164
|
+
<div class="lg:col-span-8">
|
|
165
|
+
<section class="rise" aria-label="All models">
|
|
148
166
|
<div class="flex items-center gap-2 mb-1">
|
|
149
|
-
<i data-lucide="
|
|
150
|
-
<h2 class="font-bold tracking-tight text-xl">
|
|
151
|
-
</div>
|
|
152
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)">last 14 days / tall bar input, short bar output</p>
|
|
153
|
-
<div class="rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
|
|
154
|
-
<div class="flex items-end gap-2 h-36" id="chart"></div>
|
|
155
|
-
<div class="flex gap-2 mt-2 mono text-[10px]" style="color: var(--dim)" id="chartDays"></div>
|
|
167
|
+
<i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
|
|
168
|
+
<h2 class="display font-bold tracking-tight text-xl">Models</h2>
|
|
156
169
|
</div>
|
|
170
|
+
<p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top 8</p>
|
|
171
|
+
<ol id="models" class="rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)"></ol>
|
|
157
172
|
</section>
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
</div>
|
|
174
|
+
<div class="lg:col-span-4">
|
|
175
|
+
<section id="tools" class="rise rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Top tools">
|
|
160
176
|
<div class="flex items-center gap-2 mb-1">
|
|
161
177
|
<i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
|
|
162
|
-
<h2 class="font-bold tracking-tight text-xl">Top
|
|
163
|
-
</div>
|
|
164
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)">ranked by ledger count / top 10</p>
|
|
165
|
-
<ol id="top" class="rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)"></ol>
|
|
166
|
-
<p id="emptyTop" class="mono text-sm py-8 text-center hidden" style="color: var(--dim)">No ledger rows yet. Run tersio commands to record activity.</p>
|
|
167
|
-
</section>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
<div class="lg:col-span-4 flex flex-col gap-8">
|
|
171
|
-
<section id="savings" class="rise rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); transition-delay: .1s; scroll-margin-top: 90px" aria-label="Savings">
|
|
172
|
-
<div class="flex items-center gap-2 mb-3">
|
|
173
|
-
<i data-lucide="gauge" class="size-4" style="color: var(--accent)"></i>
|
|
174
|
-
<h2 class="font-bold tracking-tight text-xl">Savings</h2>
|
|
175
|
-
</div>
|
|
176
|
-
<ul class="mono text-[13px] leading-relaxed space-y-2">
|
|
177
|
-
<li class="flex gap-2"><i data-lucide="circle-dollar-sign" class="size-4 shrink-0 mt-0.5" style="color: var(--accent)"></i><span id="usdLine">USD from per-model price table.</span></li>
|
|
178
|
-
<li class="flex gap-2"><i data-lucide="leaf" class="size-4 shrink-0 mt-0.5" style="color: var(--accent)"></i><span id="co2Line">CO2 ~est. from output tokens.</span></li>
|
|
179
|
-
<li class="flex gap-2"><i data-lucide="database" class="size-4 shrink-0 mt-0.5" style="color: var(--accent)"></i><span id="cacheLine">Cache share loads with session data.</span></li>
|
|
180
|
-
</ul>
|
|
181
|
-
</section>
|
|
182
|
-
|
|
183
|
-
<section class="rise rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); transition-delay: .18s" aria-label="Models">
|
|
184
|
-
<div class="flex items-center gap-2 mb-3">
|
|
185
|
-
<i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
|
|
186
|
-
<h2 class="font-bold tracking-tight text-xl">Models</h2>
|
|
178
|
+
<h2 class="display font-bold tracking-tight text-xl">Top tools</h2>
|
|
187
179
|
</div>
|
|
188
|
-
<
|
|
180
|
+
<p class="mono text-xs mb-4" style="color: var(--dim)">tool calls in sessions</p>
|
|
181
|
+
<ol id="top" class="mono text-[13px] space-y-3"></ol>
|
|
182
|
+
<p id="emptyTop" class="mono text-sm py-6 text-center hidden" style="color: var(--dim)">No tool calls yet.</p>
|
|
189
183
|
</section>
|
|
190
184
|
</div>
|
|
191
185
|
</main>
|
|
@@ -200,24 +194,29 @@
|
|
|
200
194
|
<a id="rawlink" href="data.json" class="underline underline-offset-2">raw data.json</a>
|
|
201
195
|
</footer>
|
|
202
196
|
</div>
|
|
197
|
+
</main>
|
|
203
198
|
|
|
204
199
|
<script>
|
|
205
200
|
(function () {
|
|
206
201
|
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
202
|
+
var hasGsap = !reduce && typeof window.gsap !== 'undefined';
|
|
203
|
+
var hasST = hasGsap && typeof window.ScrollTrigger !== 'undefined';
|
|
204
|
+
if (hasST) gsap.registerPlugin(ScrollTrigger);
|
|
207
205
|
|
|
208
206
|
function fmt(n) { return Number(n).toLocaleString('en-US'); }
|
|
207
|
+
function fmtShort(n) {
|
|
208
|
+
if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
|
|
209
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
|
|
210
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
211
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
212
|
+
return String(Math.round(n));
|
|
213
|
+
}
|
|
209
214
|
|
|
210
215
|
function countUp(el, target, money) {
|
|
211
216
|
function text(v) { return money ? '$' + v.toFixed(2) : fmt(Math.round(v)); }
|
|
212
|
-
if (
|
|
213
|
-
var
|
|
214
|
-
function
|
|
215
|
-
if (!t0) t0 = t;
|
|
216
|
-
var p = Math.min((t - t0) / dur, 1), e = 1 - Math.pow(1 - p, 3);
|
|
217
|
-
el.textContent = text(target * e);
|
|
218
|
-
if (p < 1) requestAnimationFrame(step);
|
|
219
|
-
}
|
|
220
|
-
requestAnimationFrame(step);
|
|
217
|
+
if (!hasGsap || target <= 0) { el.textContent = text(target); return; }
|
|
218
|
+
var o = { v: 0 };
|
|
219
|
+
gsap.to(o, { v: target, duration: 1.1, ease: 'power3.out', onUpdate: function () { el.textContent = text(o.v); } });
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
function tick() {
|
|
@@ -226,86 +225,257 @@
|
|
|
226
225
|
}
|
|
227
226
|
tick(); setInterval(tick, 1000);
|
|
228
227
|
|
|
228
|
+
var VENDORS = [
|
|
229
|
+
[/glm|z-ai|zhipu/i, 'Zhipu', 'Z', '#3b82f6'],
|
|
230
|
+
[/muse/i, 'Meta', 'M', '#0082fb'],
|
|
231
|
+
[/qwen|qwq/i, 'Alibaba', 'Q', '#7c3aed'],
|
|
232
|
+
[/deepseek/i, 'DeepSeek', 'D', '#4d6bfe'],
|
|
233
|
+
[/mimo/i, 'Xiaomi', 'Mi', '#ff6900'],
|
|
234
|
+
[/kimi|moonshot/i, 'Moonshot', 'K', '#a855f7'],
|
|
235
|
+
[/minimax/i, 'MiniMax', 'M', '#e11d48'],
|
|
236
|
+
[/nemotron|nvidia/i, 'NVIDIA', 'N', '#76b900'],
|
|
237
|
+
[/gpt|openai|codex/i, 'OpenAI', 'O', '#10a37f'],
|
|
238
|
+
[/claude|anthropic/i, 'Anthropic', 'A', '#d97757'],
|
|
239
|
+
[/gemini|google/i, 'Google', 'G', '#4285f4'],
|
|
240
|
+
[/grok|xai/i, 'xAI', 'X', '#71717a']
|
|
241
|
+
];
|
|
242
|
+
function vendorOf(model) {
|
|
243
|
+
for (var i = 0; i < VENDORS.length; i++) {
|
|
244
|
+
if (VENDORS[i][0].test(model)) return { name: VENDORS[i][1], mark: VENDORS[i][2], color: VENDORS[i][3] };
|
|
245
|
+
}
|
|
246
|
+
return { name: 'Other', mark: (model || '?').charAt(0).toUpperCase(), color: '#71717a' };
|
|
247
|
+
}
|
|
248
|
+
var PALETTE = ['#34d399', '#818cf8', '#22d3ee', '#fbbf24', '#f472b6', '#a78bfa', '#fb923c', '#2dd4bf'];
|
|
249
|
+
|
|
229
250
|
function dayKey(d) {
|
|
230
251
|
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
231
252
|
}
|
|
253
|
+
function modelTotal(byModel, m) {
|
|
254
|
+
var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
255
|
+
return b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
256
|
+
}
|
|
232
257
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
var DATA = null, RANGE = 'daily';
|
|
259
|
+
|
|
260
|
+
function topModels(byModel, n) {
|
|
261
|
+
return Object.keys(byModel).sort(function (a, b) { return modelTotal(byModel, b) - modelTotal(byModel, a); }).slice(0, n);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function allDays() {
|
|
265
|
+
var set = {};
|
|
266
|
+
Object.keys(DATA.byDay || {}).forEach(function (d) { set[d] = 1; });
|
|
267
|
+
Object.keys(DATA.byDayModel || {}).forEach(function (d) { set[d] = 1; });
|
|
268
|
+
return Object.keys(set).sort();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Buckets: [{ key, label, total, segs: [[model, v]] }]
|
|
272
|
+
function buckets() {
|
|
273
|
+
var days = allDays(), tops = topModels(DATA.byModel || {}, 8), out = [];
|
|
274
|
+
if (RANGE === 'daily') {
|
|
275
|
+
var last = days.slice(-60);
|
|
276
|
+
last.forEach(function (d) {
|
|
277
|
+
var per = (DATA.byDayModel || {})[d] || {}, segs = [], total = 0, other = 0;
|
|
278
|
+
tops.forEach(function (m) { var v = per[m] || 0; if (v) { segs.push([m, v]); total += v; } });
|
|
279
|
+
Object.keys(per).forEach(function (m) { if (tops.indexOf(m) < 0) other += per[m]; });
|
|
280
|
+
if (other) { segs.push(['other', other]); total += other; }
|
|
281
|
+
var dt = new Date(d + 'T12:00:00');
|
|
282
|
+
out.push({ key: d, label: isNaN(dt) ? '' : String(dt.getDate()), total: total, segs: segs });
|
|
283
|
+
});
|
|
284
|
+
document.getElementById('rangeNote').textContent = 'stacked by model / last 60 days';
|
|
285
|
+
} else if (RANGE === 'weekly') {
|
|
286
|
+
var weeks = {}, order = [];
|
|
287
|
+
days.forEach(function (d) {
|
|
288
|
+
var dt = new Date(d + 'T12:00:00'); if (isNaN(dt)) return;
|
|
289
|
+
var monday = new Date(dt); monday.setDate(dt.getDate() - ((dt.getDay() + 6) % 7));
|
|
290
|
+
var k = dayKey(monday);
|
|
291
|
+
if (!weeks[k]) { weeks[k] = {}; order.push(k); }
|
|
292
|
+
var per = (DATA.byDayModel || {})[d] || {};
|
|
293
|
+
Object.keys(per).forEach(function (m) { weeks[k][m] = (weeks[k][m] || 0) + per[m]; });
|
|
294
|
+
});
|
|
295
|
+
order.slice(-26).forEach(function (k) {
|
|
296
|
+
var per = weeks[k], segs = [], total = 0, other = 0;
|
|
297
|
+
tops.forEach(function (m) { var v = per[m] || 0; if (v) { segs.push([m, v]); total += v; } });
|
|
298
|
+
Object.keys(per).forEach(function (m) { if (tops.indexOf(m) < 0) other += per[m]; });
|
|
299
|
+
if (other) { segs.push(['other', other]); total += other; }
|
|
300
|
+
var dt = new Date(k + 'T12:00:00');
|
|
301
|
+
out.push({ key: k, label: isNaN(dt) ? '' : (dt.toLocaleString('en-US', { month: 'short' }) + ' ' + dt.getDate()), total: total, segs: segs });
|
|
302
|
+
});
|
|
303
|
+
document.getElementById('rangeNote').textContent = 'stacked by model / last 26 weeks';
|
|
304
|
+
} else {
|
|
305
|
+
var run = {}, cum = 0;
|
|
306
|
+
days.forEach(function (d) {
|
|
307
|
+
var per = (DATA.byDayModel || {})[d] || {}, segs = [], other = 0;
|
|
308
|
+
tops.forEach(function (m) {
|
|
309
|
+
var v = per[m] || 0;
|
|
310
|
+
run[m] = (run[m] || 0) + v;
|
|
311
|
+
if (run[m]) segs.push([m, run[m]]);
|
|
312
|
+
});
|
|
313
|
+
Object.keys(per).forEach(function (m) { if (tops.indexOf(m) < 0) { run[m] = (run[m] || 0) + per[m]; } });
|
|
314
|
+
other = Object.keys(run).reduce(function (a, m) { return tops.indexOf(m) < 0 ? a + run[m] : a; }, 0);
|
|
315
|
+
var tot = segs.reduce(function (a, s) { return a + s[1]; }, 0) + other;
|
|
316
|
+
if (other) segs.push(['other', other]);
|
|
317
|
+
cum = tot;
|
|
318
|
+
var dt = new Date(d + 'T12:00:00');
|
|
319
|
+
out.push({ key: d, label: isNaN(dt) ? '' : String(dt.getDate()), total: tot, segs: segs });
|
|
320
|
+
});
|
|
321
|
+
out = out.slice(-60);
|
|
322
|
+
document.getElementById('rangeNote').textContent = 'running total by model / all time';
|
|
323
|
+
}
|
|
324
|
+
return { list: out, tops: tops };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderBars() {
|
|
328
|
+
var b = buckets(), wrap = document.getElementById('bars'), axis = document.getElementById('barDays');
|
|
329
|
+
wrap.innerHTML = ''; axis.innerHTML = '';
|
|
330
|
+
var max = 1;
|
|
331
|
+
b.list.forEach(function (r) { max = Math.max(max, r.total); });
|
|
332
|
+
b.list.forEach(function (r) {
|
|
246
333
|
var col = document.createElement('div');
|
|
247
|
-
col.className = 'flex flex-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
334
|
+
col.className = 'flex-1 flex items-end h-full';
|
|
335
|
+
col.title = r.key + ': ' + fmt(r.total) + ' tokens' +
|
|
336
|
+
(r.segs.length ? '\n' + r.segs.map(function (s) { return s[0] + ' ' + fmt(s[1]); }).join('\n') : '');
|
|
337
|
+
var bar = document.createElement('div');
|
|
338
|
+
bar.className = 'mbar w-full';
|
|
339
|
+
bar.style.height = Math.max(2, Math.round(r.total / max * 100)) + '%';
|
|
340
|
+
var order = r.segs.slice().sort(function (x, y) { return y[1] - x[1]; });
|
|
341
|
+
order.forEach(function (s) {
|
|
342
|
+
var seg = document.createElement('span');
|
|
343
|
+
var ci = s[0] === 'other' ? -1 : b.tops.indexOf(s[0]);
|
|
344
|
+
seg.style.background = ci < 0 ? 'var(--dim)' : PALETTE[ci % PALETTE.length];
|
|
345
|
+
seg.style.height = Math.max(1, Math.round(s[1] / r.total * 100)) + '%';
|
|
346
|
+
bar.appendChild(seg);
|
|
347
|
+
});
|
|
348
|
+
col.appendChild(bar); wrap.appendChild(col);
|
|
349
|
+
var lb = document.createElement('span');
|
|
350
|
+
lb.className = 'flex-1 text-center truncate'; lb.textContent = r.label;
|
|
351
|
+
axis.appendChild(lb);
|
|
352
|
+
});
|
|
353
|
+
var legend = document.getElementById('legend');
|
|
354
|
+
legend.innerHTML = '';
|
|
355
|
+
b.tops.slice(0, 8).forEach(function (m, i) {
|
|
356
|
+
var s = document.createElement('span');
|
|
357
|
+
s.className = 'flex items-center gap-1.5';
|
|
358
|
+
var dot = document.createElement('span');
|
|
359
|
+
dot.style.cssText = 'width:9px;height:9px;border-radius:3px;background:' + PALETTE[i % PALETTE.length] + ';display:inline-block';
|
|
360
|
+
s.appendChild(dot);
|
|
361
|
+
s.appendChild(document.createTextNode(m));
|
|
362
|
+
legend.appendChild(s);
|
|
363
|
+
});
|
|
364
|
+
if (hasGsap) {
|
|
365
|
+
gsap.from('#bars > div', { scaleY: 0.2, opacity: 0.2, transformOrigin: 'bottom', duration: 0.5, ease: 'power2.out', stagger: 0.008, overwrite: true });
|
|
366
|
+
if (hasST) {
|
|
367
|
+
gsap.from('#modelCards > div', {
|
|
368
|
+
scale: 0.92, opacity: 0.2, duration: 0.6, ease: 'power2.out', stagger: 0.08, overwrite: true,
|
|
369
|
+
scrollTrigger: { trigger: '#modelCards', start: 'top 88%' }
|
|
370
|
+
});
|
|
256
371
|
}
|
|
257
|
-
weeks++;
|
|
258
|
-
g.appendChild(col);
|
|
259
372
|
}
|
|
260
|
-
document.getElementById('graphRange').textContent =
|
|
261
|
-
dayKey(start) + ' to ' + dayKey(today);
|
|
262
373
|
}
|
|
263
374
|
|
|
264
|
-
function
|
|
265
|
-
var
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
375
|
+
function wow(model) {
|
|
376
|
+
var days = allDays().slice(-14), cur = 0, prev = 0;
|
|
377
|
+
days.forEach(function (d, i) {
|
|
378
|
+
var v = ((DATA.byDayModel || {})[d] || {})[model] || 0;
|
|
379
|
+
if (i < days.length - 7) prev += v; else cur += v;
|
|
380
|
+
});
|
|
381
|
+
if (!prev) return cur ? 'new' : '—';
|
|
382
|
+
var p = Math.round((cur - prev) / prev * 100);
|
|
383
|
+
return (p >= 0 ? '+' : '') + p + '%';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function renderModels() {
|
|
387
|
+
var byModel = DATA.byModel || {}, tops = topModels(byModel, 8);
|
|
388
|
+
var cards = document.getElementById('modelCards');
|
|
389
|
+
cards.innerHTML = '';
|
|
390
|
+
tops.slice(0, 3).forEach(function (m, i) {
|
|
391
|
+
var v = vendorOf(m), d = wow(m);
|
|
392
|
+
var card = document.createElement('div');
|
|
393
|
+
card.className = 'rise md:col-span-4 rounded-xl p-5 overflow-hidden relative';
|
|
394
|
+
card.style.cssText = 'border: 1px solid var(--line); background: var(--panel)';
|
|
395
|
+
var ghost = document.createElement('span');
|
|
396
|
+
ghost.className = 'display pointer-events-none select-none absolute -right-2 -bottom-7 font-bold';
|
|
397
|
+
ghost.style.cssText = 'font-size: 120px; line-height: 1; opacity: .07';
|
|
398
|
+
ghost.textContent = v.mark;
|
|
399
|
+
var head = document.createElement('p');
|
|
400
|
+
head.className = 'mono text-xs mb-3'; head.style.color = 'var(--dim)';
|
|
401
|
+
head.textContent = '0' + (i + 1);
|
|
402
|
+
var row = document.createElement('div');
|
|
403
|
+
row.className = 'flex items-center gap-3';
|
|
404
|
+
var badge = document.createElement('span');
|
|
405
|
+
badge.className = 'monogram'; badge.style.background = v.color; badge.textContent = v.mark;
|
|
406
|
+
var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
|
|
407
|
+
var nm = document.createElement('p'); nm.className = 'mono text-sm font-bold truncate'; nm.textContent = m;
|
|
408
|
+
var vn = document.createElement('p'); vn.className = 'mono text-xs truncate'; vn.style.color = 'var(--dim)'; vn.textContent = v.name;
|
|
409
|
+
mid.appendChild(nm); mid.appendChild(vn);
|
|
410
|
+
var tot = document.createElement('p'); tot.className = 'mono font-bold text-lg shrink-0'; tot.textContent = fmtShort(modelTotal(byModel, m));
|
|
411
|
+
row.appendChild(badge); row.appendChild(mid); row.appendChild(tot);
|
|
412
|
+
var delta = document.createElement('p');
|
|
413
|
+
delta.className = 'mono text-xs mt-3';
|
|
414
|
+
delta.style.color = d.charAt(0) === '-' ? '#f87171' : 'var(--accent)';
|
|
415
|
+
delta.textContent = d;
|
|
416
|
+
card.appendChild(ghost); card.appendChild(head); card.appendChild(row); card.appendChild(delta);
|
|
417
|
+
cards.appendChild(card);
|
|
418
|
+
});
|
|
419
|
+
var ol = document.getElementById('models');
|
|
420
|
+
ol.innerHTML = '';
|
|
272
421
|
var max = 1;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
var
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
var
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
var
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
422
|
+
tops.forEach(function (m) { max = Math.max(max, modelTotal(byModel, m)); });
|
|
423
|
+
tops.forEach(function (m, i) {
|
|
424
|
+
var v = vendorOf(m);
|
|
425
|
+
var li = document.createElement('li');
|
|
426
|
+
li.className = 'tilt flex items-center gap-3 px-4 py-3' + (i < tops.length - 1 ? ' rowline' : '');
|
|
427
|
+
var badge = document.createElement('span');
|
|
428
|
+
badge.className = 'monogram'; badge.style.cssText = 'width:28px;height:28px;font-size:11px;border-radius:8px;background:' + v.color;
|
|
429
|
+
badge.textContent = v.mark;
|
|
430
|
+
badge.title = v.name;
|
|
431
|
+
var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
|
|
432
|
+
var nm = document.createElement('p'); nm.className = 'mono text-sm truncate'; nm.textContent = m;
|
|
433
|
+
var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
|
|
434
|
+
var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
|
|
435
|
+
fill.style.width = Math.max(2, Math.round(modelTotal(byModel, m) / max * 100)) + '%';
|
|
436
|
+
track.appendChild(fill); mid.appendChild(nm); mid.appendChild(track);
|
|
437
|
+
var n = document.createElement('span'); n.className = 'mono font-bold shrink-0'; n.textContent = fmt(modelTotal(byModel, m));
|
|
438
|
+
li.appendChild(badge); li.appendChild(mid); li.appendChild(n);
|
|
439
|
+
ol.appendChild(li);
|
|
290
440
|
});
|
|
441
|
+
if (!tops.length) ol.innerHTML = '<li class="mono text-sm px-4 py-6" style="color: var(--dim)">no session tokens yet</li>';
|
|
291
442
|
}
|
|
292
443
|
|
|
293
|
-
function
|
|
294
|
-
var
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
444
|
+
function renderTools() {
|
|
445
|
+
var ol = document.getElementById('top');
|
|
446
|
+
ol.innerHTML = '';
|
|
447
|
+
var tools = DATA.byTool || [], max = 1;
|
|
448
|
+
tools.forEach(function (r) { max = Math.max(max, r[1]); });
|
|
449
|
+
tools.forEach(function (r) {
|
|
450
|
+
var li = document.createElement('li');
|
|
451
|
+
var top = document.createElement('div');
|
|
452
|
+
top.className = 'flex justify-between gap-2';
|
|
453
|
+
var nm = document.createElement('span'); nm.className = 'truncate'; nm.textContent = r[0];
|
|
454
|
+
var n = document.createElement('span'); n.className = 'font-bold'; n.textContent = fmt(r[1]);
|
|
455
|
+
top.appendChild(nm); top.appendChild(n);
|
|
456
|
+
var track = document.createElement('div'); track.className = 'bar-track mt-1 h-1 overflow-hidden';
|
|
457
|
+
var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
|
|
458
|
+
fill.style.width = Math.max(2, Math.round(r[1] / max * 100)) + '%';
|
|
459
|
+
track.appendChild(fill);
|
|
460
|
+
li.appendChild(top); li.appendChild(track);
|
|
461
|
+
ol.appendChild(li);
|
|
462
|
+
});
|
|
463
|
+
document.getElementById('emptyTop').classList.toggle('hidden', tools.length > 0);
|
|
464
|
+
}
|
|
302
465
|
|
|
303
|
-
|
|
466
|
+
function renderStrip(t) {
|
|
467
|
+
var defs = [
|
|
468
|
+
['input', t.input, 'arrow-down-to-line'],
|
|
469
|
+
['output', t.output, 'arrow-up-from-line'],
|
|
470
|
+
['cache read', t.cacheRead, 'hard-drive-download'],
|
|
471
|
+
['cache write', t.cacheWrite, 'hard-drive-upload']
|
|
472
|
+
].filter(function (b) { return b[1] > 0; });
|
|
473
|
+
if (!defs.length) defs = [['input', 0, 'arrow-down-to-line'], ['output', 0, 'arrow-up-from-line']];
|
|
304
474
|
var strip = document.getElementById('tokens');
|
|
305
475
|
strip.innerHTML = '';
|
|
306
|
-
|
|
476
|
+
defs.forEach(function (b, i) {
|
|
307
477
|
var cell = document.createElement('div');
|
|
308
|
-
cell.className = 'tilt px-4 py-4' + (i > 0 ? ' sm:border-l' : '');
|
|
478
|
+
cell.className = 'tilt flex-1 px-4 py-4' + (i > 0 ? ' sm:border-l' : '');
|
|
309
479
|
cell.style.borderColor = 'var(--line)';
|
|
310
480
|
cell.innerHTML = '<div class="flex items-center gap-2 mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">' +
|
|
311
481
|
'<i data-lucide="' + b[2] + '" class="size-3.5"></i><span></span></div>' +
|
|
@@ -314,63 +484,60 @@
|
|
|
314
484
|
cell.querySelector('p').textContent = fmt(b[1]);
|
|
315
485
|
strip.appendChild(cell);
|
|
316
486
|
});
|
|
487
|
+
}
|
|
317
488
|
|
|
318
|
-
|
|
319
|
-
renderChart(d.byDay || {});
|
|
320
|
-
|
|
321
|
-
var cacheShare = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
|
|
322
|
-
document.getElementById('usdLine').textContent =
|
|
323
|
-
'$' + (d.usd || 0).toFixed(2) + (d.priced ? ' measured (price table).' : ' incl. default pricing.');
|
|
324
|
-
document.getElementById('co2Line').textContent =
|
|
325
|
-
'~' + (d.co2g || 0).toFixed(1) + 'g CO2 est. from output tokens.';
|
|
326
|
-
document.getElementById('cacheLine').textContent =
|
|
327
|
-
cacheShare + '% of tokens served from cache.';
|
|
489
|
+
function money(v) { return '$' + v.toFixed(2); }
|
|
328
490
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
491
|
+
function render(d) {
|
|
492
|
+
DATA = d;
|
|
493
|
+
var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
494
|
+
var total = t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
495
|
+
document.getElementById('meta').textContent = fmt(total) + ' session tokens / ' + d.total + ' ledger rows';
|
|
496
|
+
var sub = document.getElementById('herosub');
|
|
497
|
+
sub.innerHTML = '';
|
|
498
|
+
('input ' + fmt(t.input) + ' · output ' + fmt(t.output) + ' · cache read ' + fmt(t.cacheRead) +
|
|
499
|
+
(t.cacheWrite ? ' · cache write ' + fmt(t.cacheWrite) : '') + ' · ' + money(d.usd || 0)).split(' ').forEach(function (w) {
|
|
500
|
+
var s = document.createElement('span'); s.textContent = w + ' ';
|
|
501
|
+
sub.appendChild(s);
|
|
339
502
|
});
|
|
340
|
-
|
|
341
|
-
|
|
503
|
+
var totalEl = document.getElementById('total');
|
|
504
|
+
if (hasGsap) {
|
|
505
|
+
var o = { v: 0 };
|
|
506
|
+
gsap.to(o, { v: total, duration: 1.2, ease: 'power3.out', onUpdate: function () { totalEl.textContent = fmt(o.v); } });
|
|
507
|
+
} else {
|
|
508
|
+
totalEl.textContent = fmt(total);
|
|
342
509
|
}
|
|
510
|
+
countUp(document.getElementById('usd'), d.usd || 0, true);
|
|
511
|
+
document.getElementById('usdNote').textContent = d.priced ? 'per-model price table' : 'incl. default pricing';
|
|
512
|
+
countUp(document.getElementById('saved'), d.savedUsd || 0, true);
|
|
513
|
+
document.getElementById('co2').textContent = '~' + (d.co2g || 0).toFixed(1) + 'g';
|
|
514
|
+
var share = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
|
|
515
|
+
document.getElementById('cacheShare').textContent = share + '%';
|
|
516
|
+
document.getElementById('msgs').textContent = fmt(d.messages || 0);
|
|
343
517
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
(
|
|
348
|
-
(d.byDetail || []).forEach(function (row, i) {
|
|
349
|
-
var li = document.createElement('li');
|
|
350
|
-
li.className = 'tilt flex items-center gap-3 px-4 py-3' + (i < d.byDetail.length - 1 ? ' rowline' : '');
|
|
351
|
-
var rank = document.createElement('span');
|
|
352
|
-
rank.className = 'mono text-xs w-6 shrink-0'; rank.style.color = 'var(--dim)';
|
|
353
|
-
rank.textContent = String(i + 1).padStart(2, '0');
|
|
354
|
-
var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
|
|
355
|
-
var name = document.createElement('p'); name.className = 'mono text-sm truncate'; name.textContent = row[0];
|
|
356
|
-
var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
|
|
357
|
-
var fill = document.createElement('div'); fill.className = 'bar-fill grow h-full';
|
|
358
|
-
fill.style.width = Math.max(2, Math.round(row[1] / max * 100)) + '%';
|
|
359
|
-
track.appendChild(fill); mid.appendChild(name); mid.appendChild(track);
|
|
360
|
-
var n = document.createElement('span'); n.className = 'mono font-bold text-lg shrink-0'; n.textContent = String(row[1]);
|
|
361
|
-
li.appendChild(rank); li.appendChild(mid); li.appendChild(n);
|
|
362
|
-
ol.appendChild(li);
|
|
363
|
-
});
|
|
364
|
-
document.getElementById('emptyTop').classList.toggle('hidden', (d.total || 0) > 0);
|
|
365
|
-
ol.classList.toggle('hidden', (d.total || 0) === 0);
|
|
518
|
+
renderStrip(t);
|
|
519
|
+
renderBars();
|
|
520
|
+
renderModels();
|
|
521
|
+
renderTools();
|
|
366
522
|
|
|
367
523
|
if (window.lucide) lucide.createIcons();
|
|
368
|
-
|
|
524
|
+
if (hasGsap) {
|
|
525
|
+
gsap.from('.hero-in', { y: 26, opacity: 0, duration: 0.8, ease: 'power3.out', stagger: 0.08 });
|
|
526
|
+
// Scrub: hero sub words fade in sequence on scroll.
|
|
527
|
+
if (hasST) {
|
|
528
|
+
gsap.fromTo('#herosub span', { opacity: 0.15 }, {
|
|
529
|
+
opacity: 1, ease: 'none', stagger: 0.05,
|
|
530
|
+
scrollTrigger: { trigger: '#herosub', start: 'top 92%', end: 'top 55%', scrub: true }
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
observe();
|
|
535
|
+
}
|
|
369
536
|
}
|
|
370
537
|
|
|
371
538
|
var seen = new WeakSet();
|
|
372
539
|
function observe() {
|
|
373
|
-
var els = document.querySelectorAll('.rise
|
|
540
|
+
var els = document.querySelectorAll('.rise');
|
|
374
541
|
if (!('IntersectionObserver' in window) || reduce) {
|
|
375
542
|
els.forEach(function (el) { el.classList.add('in'); });
|
|
376
543
|
return;
|
|
@@ -385,10 +552,19 @@
|
|
|
385
552
|
els.forEach(function (el) { if (!seen.has(el)) io.observe(el); });
|
|
386
553
|
}
|
|
387
554
|
|
|
555
|
+
document.querySelectorAll('.seg button').forEach(function (btn) {
|
|
556
|
+
btn.addEventListener('click', function () {
|
|
557
|
+
document.querySelectorAll('.seg button').forEach(function (b) { b.classList.remove('on'); });
|
|
558
|
+
btn.classList.add('on');
|
|
559
|
+
RANGE = btn.getAttribute('data-range');
|
|
560
|
+
if (DATA) renderBars();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
388
564
|
function load() {
|
|
389
565
|
document.getElementById('meta').textContent = 'connecting to ledger';
|
|
390
566
|
fetch('data.json').then(function (r) { return r.json(); }).then(render).catch(function () {
|
|
391
|
-
document.getElementById('meta').textContent = 'No live data.
|
|
567
|
+
document.getElementById('meta').textContent = 'No live data. Open with: tersio gain --open';
|
|
392
568
|
document.getElementById('rawlink').style.display = 'none';
|
|
393
569
|
document.getElementById('emptyTop').classList.remove('hidden');
|
|
394
570
|
if (window.lucide) lucide.createIcons();
|
package/cli/dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli/dashboard.ts —
|
|
1
|
+
// cli/dashboard.ts — gain dashboard. Serves the single plain-HTML
|
|
2
2
|
// template (no deps, no build) plus a /data.json endpoint from the ledger.
|
|
3
3
|
// Binds 127.0.0.1 only; --export writes a file://-ready file instead.
|
|
4
4
|
// Note: no Promise.withResolvers here — engines still allow Node 20.12,
|
|
@@ -35,7 +35,7 @@ async function runDashboard(options) {
|
|
|
35
35
|
.replace('href="brand.webp"', `href="${await faviconDataUri()}"`)
|
|
36
36
|
.replace('src="brand.webp"', `src="${await faviconDataUri()}"`);
|
|
37
37
|
await fs.writeFile(options.exportFile, inline, 'utf8');
|
|
38
|
-
console.log(`[ok]
|
|
38
|
+
console.log(`[ok] gain exported → ${options.exportFile}`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
const html = await withInteractiveSpinner('Loading dashboard template', templateHtml);
|
|
@@ -64,7 +64,7 @@ async function runDashboard(options) {
|
|
|
64
64
|
const address = server.address();
|
|
65
65
|
const port = typeof address === 'object' && address ? address.port : options.port;
|
|
66
66
|
const url = `http://127.0.0.1:${port}`;
|
|
67
|
-
console.log(`[ok]
|
|
67
|
+
console.log(`[ok] gain live → ${url} (Ctrl-C to stop)`);
|
|
68
68
|
if (options.open)
|
|
69
69
|
openBrowser(url);
|
|
70
70
|
});
|
package/cli/install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { BUN_BIN_DIR, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, SCOPE_MAP, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, scopeFlag, verbose, yes, dashboardExport,
|
|
6
|
+
import { BUN_BIN_DIR, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, SCOPE_MAP, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, scopeFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
7
|
import { ask, askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
|
|
8
8
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
9
9
|
import { runUninstall } from './uninstall.js';
|
|
@@ -571,7 +571,6 @@ async function runCommandMenu() {
|
|
|
571
571
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
572
572
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
573
573
|
{ value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
|
|
574
|
-
{ value: 'dashboard', label: 'Serve dashboard', hint: 'localhost server, no browser' },
|
|
575
574
|
{ value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
|
|
576
575
|
], 'install');
|
|
577
576
|
if (choice.status !== 'selected') {
|
|
@@ -601,11 +600,7 @@ async function runCommandMenu() {
|
|
|
601
600
|
closeRL();
|
|
602
601
|
break;
|
|
603
602
|
case 'gain':
|
|
604
|
-
await runDashboard({ port: dashboardPort, open: true, exportFile:
|
|
605
|
-
closeRL();
|
|
606
|
-
break;
|
|
607
|
-
case 'dashboard':
|
|
608
|
-
await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport });
|
|
603
|
+
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
|
|
609
604
|
closeRL();
|
|
610
605
|
break;
|
|
611
606
|
case 'uninstall':
|
package/cli/usage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
-
import { co2Grams, importSessionTokens, readUsage, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
2
|
+
import { co2Grams, importSessionTokens, priceFor, readUsage, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
3
|
import { withInteractiveSpinner } from './interactive.js';
|
|
4
4
|
export function summarizeUsage(rows) {
|
|
5
5
|
const byKind = {};
|
|
@@ -12,12 +12,15 @@ export function summarizeUsage(rows) {
|
|
|
12
12
|
const session = importSessionTokens();
|
|
13
13
|
let usd = 0;
|
|
14
14
|
let priced = true;
|
|
15
|
+
let savedUsd = 0;
|
|
15
16
|
for (const [model, t] of Object.entries(session.byModel)) {
|
|
16
17
|
const c = usdCost(t, model);
|
|
17
18
|
usd += c.usd;
|
|
18
19
|
if (!c.priced)
|
|
19
20
|
priced = false;
|
|
21
|
+
savedUsd += (t.cacheRead / 1e6) * priceFor(model).price.input;
|
|
20
22
|
}
|
|
23
|
+
const byTool = Object.entries(session.byTool).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
21
24
|
return {
|
|
22
25
|
total: rows.length,
|
|
23
26
|
byKind,
|
|
@@ -28,8 +31,12 @@ export function summarizeUsage(rows) {
|
|
|
28
31
|
tokens: session.totals,
|
|
29
32
|
byModel: session.byModel,
|
|
30
33
|
byDay: session.byDay,
|
|
34
|
+
byDayModel: session.byDayModel,
|
|
35
|
+
byTool,
|
|
31
36
|
usd,
|
|
32
37
|
priced,
|
|
38
|
+
savedUsd,
|
|
39
|
+
costMeasured: session.costMeasured,
|
|
33
40
|
co2g: co2Grams(session.totals.output),
|
|
34
41
|
};
|
|
35
42
|
}
|
|
@@ -44,7 +51,7 @@ function printReport(report) {
|
|
|
44
51
|
}
|
|
45
52
|
const t = report.tokens;
|
|
46
53
|
console.log(` Tokens input ${fmt(t.input)} · output ${fmt(t.output)} · cache read ${fmt(t.cacheRead)} · cache write ${fmt(t.cacheWrite)} (${report.messages} assistant messages)`);
|
|
47
|
-
console.log(` Cost $${report.usd.toFixed(2)}${report.priced ? '' : ' (includes default pricing for unknown models)'} · CO2 ~${report.co2g.toFixed(1)}g (est.)`);
|
|
54
|
+
console.log(` Cost $${report.usd.toFixed(2)}${report.priced ? '' : ' (includes default pricing for unknown models)'} · ~$${report.savedUsd.toFixed(2)} saved by cache (est.) · CO2 ~${report.co2g.toFixed(1)}g (est.)`);
|
|
48
55
|
const models = Object.entries(report.byModel).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output)).slice(0, 5);
|
|
49
56
|
if (models.length) {
|
|
50
57
|
console.log(' By model');
|
|
@@ -52,6 +59,12 @@ function printReport(report) {
|
|
|
52
59
|
console.log(` ${model}: in ${fmt(b.input)} · out ${fmt(b.output)} · cache r/w ${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`);
|
|
53
60
|
}
|
|
54
61
|
}
|
|
62
|
+
if (report.byTool.length) {
|
|
63
|
+
console.log(' Top tools');
|
|
64
|
+
for (const [tool, n] of report.byTool) {
|
|
65
|
+
console.log(` ${n}× ${tool}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
55
68
|
if (report.total) {
|
|
56
69
|
console.log(' Activity');
|
|
57
70
|
for (const [kind, n] of Object.entries(report.byKind)) {
|
|
@@ -61,7 +74,6 @@ function printReport(report) {
|
|
|
61
74
|
for (const [detail, n] of report.byDetail) {
|
|
62
75
|
console.log(` ${n}× ${detail}`);
|
|
63
76
|
}
|
|
64
|
-
console.log(` Total rows: ${report.total} · last write: ${report.lastWrite}`);
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
async function runUsage() {
|
|
@@ -64,8 +64,11 @@ function walkJsonl(dir, out, cap) {
|
|
|
64
64
|
export function importSessionTokens() {
|
|
65
65
|
const byModel = {};
|
|
66
66
|
const byDay = {};
|
|
67
|
+
const byDayModel = {};
|
|
68
|
+
const byTool = {};
|
|
67
69
|
const totals = zeroBreakdown();
|
|
68
70
|
let messages = 0;
|
|
71
|
+
let costMeasured = 0;
|
|
69
72
|
const files = [];
|
|
70
73
|
walkJsonl(sessionsDir(), files, 2000);
|
|
71
74
|
for (const file of files) {
|
|
@@ -92,13 +95,44 @@ export function importSessionTokens() {
|
|
|
92
95
|
if (day) {
|
|
93
96
|
byDay[day] ??= zeroBreakdown();
|
|
94
97
|
addInto(byDay[day], msg.usage);
|
|
98
|
+
const n = msg.usage;
|
|
99
|
+
const sum = ['input', 'output', 'cacheRead', 'cacheWrite'].reduce((a, k) => a + (typeof n[k] === 'number' && Number.isFinite(n[k]) ? Math.floor(n[k]) : 0), 0);
|
|
100
|
+
if (sum > 0) {
|
|
101
|
+
byDayModel[day] ??= {};
|
|
102
|
+
byDayModel[day][model] = (byDayModel[day][model] ?? 0) + sum;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (typeof msg.usage.cost === 'number' && Number.isFinite(msg.usage.cost))
|
|
106
|
+
costMeasured += msg.usage.cost;
|
|
107
|
+
for (const part of msg.content ?? []) {
|
|
108
|
+
if (part?.type === 'toolCall' && typeof part.name === 'string' && part.name) {
|
|
109
|
+
const key = part.name === 'bash' ? `bash:${leadBinary(part.arguments?.command)}` : part.name;
|
|
110
|
+
byTool[key] = (byTool[key] ?? 0) + 1;
|
|
111
|
+
}
|
|
95
112
|
}
|
|
96
113
|
messages += 1;
|
|
97
114
|
}
|
|
98
115
|
catch { /* skip corrupt lines */ }
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
|
-
return { messages, totals, byModel, byDay };
|
|
118
|
+
return { messages, totals, byModel, byDay, byDayModel, byTool, costMeasured };
|
|
119
|
+
}
|
|
120
|
+
// Lead binary of a shell string: first segment head past `cd` chains and
|
|
121
|
+
// VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
|
|
122
|
+
const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
|
|
123
|
+
function leadBinary(command) {
|
|
124
|
+
if (typeof command !== 'string' || !command.trim())
|
|
125
|
+
return 'bash';
|
|
126
|
+
for (const seg of command.split(/&&|;|\n|\|(?!\|)/)) {
|
|
127
|
+
const toks = seg.trim().split(/\s+/).filter(Boolean);
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < toks.length && (toks[i] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i])))
|
|
130
|
+
i++;
|
|
131
|
+
const head = (toks[i] ?? '').replace(/^['"]|['"]$/g, '').split('/').pop() ?? '';
|
|
132
|
+
if (head && !SKIP_HEADS.includes(head))
|
|
133
|
+
return head;
|
|
134
|
+
}
|
|
135
|
+
return 'bash';
|
|
102
136
|
}
|
|
103
137
|
const PRICE_TABLE = [
|
|
104
138
|
{ match: ':free', price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
|
|
@@ -43,7 +43,7 @@ function usageSummary() {
|
|
|
43
43
|
async function openGainDashboard(pi, ctx) {
|
|
44
44
|
const file = path.join(os.tmpdir(), `tersio-gain-${Date.now()}.html`);
|
|
45
45
|
try {
|
|
46
|
-
const exported = await pi.exec?.('tersio', ['
|
|
46
|
+
const exported = await pi.exec?.('tersio', ['gain', '--export', file], { cwd: ctx?.cwd });
|
|
47
47
|
if (!exported || exported.code !== 0)
|
|
48
48
|
throw new Error((exported?.stderr || 'export failed').trim());
|
|
49
49
|
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
@@ -51,7 +51,7 @@ async function openGainDashboard(pi, ctx) {
|
|
|
51
51
|
ctx?.ui?.notify?.(`tersio gain: dashboard opened in your browser.`, 'info');
|
|
52
52
|
}
|
|
53
53
|
catch (e) {
|
|
54
|
-
ctx?.ui?.notify?.(`tersio gain: could not open dashboard (${e.message}). Run 'tersio
|
|
54
|
+
ctx?.ui?.notify?.(`tersio gain: could not open dashboard (${e.message}). Run 'tersio gain --open' in a shell.`, 'warning');
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
export default function tersioCommandsExtension(pi) {
|
package/package.json
CHANGED
package/tersio.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// tersio.ts — Install Tersio (caveman/rtk/ponytail) add-ons on any OMP device.
|
|
3
3
|
// Usage: node tersio.js [install|update|reinstall|doctor|uninstall|version|help] [options]
|
|
4
4
|
// Requires: node/npm and omp CLI
|
|
5
|
-
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg,
|
|
5
|
+
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, gain, dashboardExport, dashboardOpen, dashboardPort, doctor, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
|
|
6
6
|
import { closeRL } from './cli/interactive.js';
|
|
7
7
|
import { runInstall } from './cli/install.js';
|
|
8
8
|
import { runDoctor } from './cli/doctor.js';
|
|
@@ -19,7 +19,7 @@ Commands:
|
|
|
19
19
|
reinstall Clean and reinstall the user-scope add-ons
|
|
20
20
|
doctor Check the current installation
|
|
21
21
|
usage Ledger-backed usage + savings report
|
|
22
|
-
|
|
22
|
+
gain Open the gain dashboard (localhost only)
|
|
23
23
|
uninstall Remove the managed extensions
|
|
24
24
|
version Print the package version
|
|
25
25
|
help Show this help
|
|
@@ -35,9 +35,9 @@ Options:
|
|
|
35
35
|
--yes, -y
|
|
36
36
|
--dry-run
|
|
37
37
|
--verbose
|
|
38
|
-
--port <n> (
|
|
39
|
-
--open (
|
|
40
|
-
--export <file> (
|
|
38
|
+
--port <n> (gain: pin port, default ephemeral)
|
|
39
|
+
--open (gain: open browser)
|
|
40
|
+
--export <file> (gain: write HTML file instead of serving)
|
|
41
41
|
--version, -v
|
|
42
42
|
--help, -h`);
|
|
43
43
|
}
|
|
@@ -74,7 +74,7 @@ async function main() {
|
|
|
74
74
|
closeRL();
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
if (
|
|
77
|
+
if (gain) {
|
|
78
78
|
await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport });
|
|
79
79
|
closeRL();
|
|
80
80
|
return;
|