@krtclcdy/tersio 2.5.0 → 2.6.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 +33 -50
- package/cli/brand.webp +0 -0
- package/cli/common.js +7 -2
- package/cli/dashboard.html +404 -0
- package/cli/dashboard.js +72 -0
- package/cli/doctor.js +4 -0
- package/cli/usage.js +71 -0
- package/extensions/ai-addons-updater/index.js +28 -23
- package/extensions/shared/usage-ledger.js +156 -0
- package/extensions/tersio-commands/index.js +148 -0
- package/package.json +10 -1
- package/tersio.js +18 -1
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ omp plugin features @krtclcdy/tersio --disable rtk
|
|
|
33
33
|
Then restart OMP and enable a preset:
|
|
34
34
|
|
|
35
35
|
```text
|
|
36
|
-
/combo medium
|
|
36
|
+
/tersio combo medium
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Individual toggles: `/caveman full` · `/rtk on` · `/ponytail full`. Everything starts off until you enable it.
|
|
39
|
+
Individual toggles: `/tersio caveman full` · `/tersio rtk on` · `/tersio ponytail full`. Everything starts off until you enable it.
|
|
40
40
|
|
|
41
41
|
Session-start defaults (prompted during `install`, or flags):
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ Session-start defaults (prompted during `install`, or flags):
|
|
|
44
44
|
tersio install --combo-default balanced --caveman-default lite --rtk-default on
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Defaults apply to fresh sessions only — anything persisted with `/combo`, `/caveman`, or `/rtk` wins. Stored as plugin settings (`omp plugin config get @krtclcdy/tersio comboDefault`).
|
|
47
|
+
Defaults apply to fresh sessions only — anything persisted with `/tersio combo`, `/tersio caveman`, or `/tersio rtk` wins. Stored as plugin settings (`omp plugin config get @krtclcdy/tersio comboDefault`).
|
|
48
48
|
|
|
49
49
|
## What it installs
|
|
50
50
|
|
|
@@ -54,7 +54,7 @@ Defaults apply to fresh sessions only — anything persisted with `/combo`, `/ca
|
|
|
54
54
|
| **RTK** | Routes noisy shell commands through the RTK binary for compact output |
|
|
55
55
|
| **Ponytail** | Minimal, YAGNI-oriented code decisions |
|
|
56
56
|
| **Combo** | Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max` |
|
|
57
|
-
| **Updater** | `/
|
|
57
|
+
| **Updater** | `/tersio check` and `/tersio update` cover Ponytail, RTK, and Caveman in-session, with dry-run and backups |
|
|
58
58
|
|
|
59
59
|
User-level installs also register the package in `~/.omp/plugins` (visible in OMP **Settings → Plugins**), directly through the plugin manifest. Active modes reassert after Ponytail's prompt block on every top-level turn, including after history compaction.
|
|
60
60
|
|
|
@@ -81,6 +81,8 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead
|
|
|
81
81
|
| `tersio update` | Refresh the CLI, extensions, and add-ons (RTK binary, Caveman rule, Ponytail) |
|
|
82
82
|
| `tersio reinstall` | Fresh install, preserving the Ponytail package |
|
|
83
83
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
|
|
84
|
+
| `tersio usage` | Ledger-backed usage + savings report |
|
|
85
|
+
| `tersio dashboard` | Serve the gain dashboard on localhost (`--open`, `--export <file>`, `--port <n>`) |
|
|
84
86
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
|
|
85
87
|
| `tersio version` | Print version |
|
|
86
88
|
|
|
@@ -88,63 +90,44 @@ Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--scope`, `--combo-default`/`--c
|
|
|
88
90
|
|
|
89
91
|
## Commands reference
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
Everything runs under one root. Bare `/tersio` prints status.
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
| Command | Purpose |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `/tersio combo off\|medium\|balanced\|max\|status` | preset for all three modes (start here) |
|
|
98
|
+
| `/tersio caveman lite\|full\|ultra\|wenyan\|off\|status` | terse-reply mode |
|
|
99
|
+
| `/tersio rtk on\|off\|status` | compact shell-output toggle |
|
|
100
|
+
| `/tersio ponytail off\|lite\|full\|ultra\|review\|status` | minimal-code level |
|
|
101
|
+
| `/tersio status` | active modes + combo level |
|
|
102
|
+
| `/tersio check` | add-on version check (same as old `/ai-addons check`) |
|
|
103
|
+
| `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | update add-ons, preview with dry-run |
|
|
104
|
+
| `/tersio gain` | savings summary + dashboard hint |
|
|
105
|
+
| `/tersio usage` | ledger report for this machine |
|
|
106
|
+
| `/tersio help` | this table |
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
### Combo — one preset for all three
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
Start here: one setting drives all three modes (`medium` = lite/lite/on, `balanced` = full/full/on, `max` = ultra/ultra/on, `off` = normal). Tweaking a mode individually drops to a `custom` mix.
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
/rtk on enable compact RTK output
|
|
108
|
-
/rtk off disable
|
|
109
|
-
/rtk status show current state
|
|
110
|
-
```
|
|
112
|
+
### Caveman — terse replies
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
Pleasantries and restatements go; substance stays. Ladder: `lite`, `full`, `ultra` (fragments only), `wenyan` (compressed classical-Chinese-inspired register).
|
|
113
115
|
|
|
114
|
-
###
|
|
116
|
+
### RTK — compact shell output
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
/ponytail lite light guidance
|
|
118
|
-
/ponytail full full YAGNI enforcement
|
|
119
|
-
/ponytail ultra aggressive simplification
|
|
120
|
-
/ponytail off disable
|
|
121
|
-
/ponytail status show current state
|
|
122
|
-
```
|
|
118
|
+
A real binary, not a prompt trick: `rtk git status|diff|grep|test|tsc|lint` strips noise before output reaches the model. Exact bytes (checksums, patches) bypass it by policy.
|
|
123
119
|
|
|
124
|
-
###
|
|
120
|
+
### Ponytail — minimal code (YAGNI)
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
/ai-addons check check all add-on versions
|
|
128
|
-
/ai-addons status same as check
|
|
129
|
-
/ai-addons update ponytail update Ponytail
|
|
130
|
-
/ai-addons update rtk update the RTK binary
|
|
131
|
-
/ai-addons update caveman update the Caveman rule
|
|
132
|
-
/ai-addons update all update all three
|
|
133
|
-
/ai-addons update all --dry-run preview without changes
|
|
134
|
-
```
|
|
122
|
+
You Aren't Gonna Need It: smallest working change, no speculative abstractions, no new deps for stdlib jobs. `lite` nudges, `full` enforces, `ultra` challenges the requirement, `review` audits for over-engineering.
|
|
135
123
|
|
|
136
|
-
|
|
124
|
+
`/tersio` persists state and reloads OMP without emitting separate command messages. Presets propagate to task subagents and light the Combo footer indicator; mixed individual settings report via `/tersio status`.
|
|
137
125
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/combo balanced caveman=full, rtk=on, ponytail=full
|
|
142
|
-
/combo max caveman=ultra, rtk=on, ponytail=ultra
|
|
143
|
-
/combo status show the level and underlying modes
|
|
144
|
-
/combo help show available levels
|
|
145
|
-
```
|
|
126
|
+
### Legacy aliases (still work)
|
|
127
|
+
|
|
128
|
+
The pre-`/tersio` commands remain registered: `/caveman`, `/rtk`, `/combo`, `/ai-addons`. Prefer `/tersio` in new muscle memory; help output lists the aliases as deprecated-but-working.
|
|
146
129
|
|
|
147
|
-
|
|
130
|
+
Usage rows land in `~/.omp/plugins/tersio-usage.jsonl` (local only); `tersio dashboard` serves them as a plain-HTML page on 127.0.0.1.
|
|
148
131
|
|
|
149
132
|
## Files and backups
|
|
150
133
|
|
|
@@ -176,7 +159,7 @@ npm exec --yes --prefer-online --package=@krtclcdy/tersio@latest -- tersio insta
|
|
|
176
159
|
|
|
177
160
|
**RTK missing or not executable:** run `tersio reinstall`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
|
|
178
161
|
|
|
179
|
-
**Checksum warning or failure:** the installer aborts on RTK checksum mismatch but warns and continues when checksum metadata is unavailable; `/
|
|
162
|
+
**Checksum warning or failure:** the installer aborts on RTK checksum mismatch but warns and continues when checksum metadata is unavailable; `/tersio update rtk` aborts when metadata is missing.
|
|
180
163
|
|
|
181
164
|
## License
|
|
182
165
|
|
package/cli/brand.webp
ADDED
|
Binary file
|
package/cli/common.js
CHANGED
|
@@ -46,7 +46,7 @@ function flagValue(name) {
|
|
|
46
46
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
47
47
|
}
|
|
48
48
|
// --- CLI flags ---
|
|
49
|
-
const COMMANDS = new Set(['install', 'update', 'reinstall', 'doctor', 'uninstall', 'version', 'help']);
|
|
49
|
+
const COMMANDS = new Set(['install', 'update', 'reinstall', 'doctor', 'uninstall', 'usage', 'dashboard', 'version', 'help']);
|
|
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;
|
|
@@ -61,6 +61,11 @@ const dryRun = args.includes('--dry-run');
|
|
|
61
61
|
const yes = args.includes('--yes') || args.includes('-y') || install || update || reinstall || applyUpdate;
|
|
62
62
|
const verbose = args.includes('--verbose');
|
|
63
63
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
64
|
+
const usage = command === 'usage' || args.includes('--usage');
|
|
65
|
+
const dashboard = command === 'dashboard' || args.includes('--dashboard');
|
|
66
|
+
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
67
|
+
const dashboardOpen = args.includes('--open');
|
|
68
|
+
const dashboardExport = flagValue('--export') ?? null;
|
|
64
69
|
const uninstall = command === 'uninstall' || args.includes('--uninstall');
|
|
65
70
|
const removePonytail = args.includes('--remove-ponytail');
|
|
66
71
|
const keepPonytail = args.includes('--keep-ponytail');
|
|
@@ -249,4 +254,4 @@ function relTime(ageMs) {
|
|
|
249
254
|
const weeks = Math.floor(days / 7);
|
|
250
255
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
251
256
|
}
|
|
252
|
-
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, 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, };
|
|
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, dashboard, 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, };
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="dark light">
|
|
7
|
+
<title>Tersio Gain Dashboard</title>
|
|
8
|
+
<link rel="icon" type="image/png" href="brand.webp">
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
10
|
+
<script src="https://unpkg.com/lucide@latest"></script>
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #f4f4f5; --panel: #ffffff; --ink: #18181b; --dim: #52525b;
|
|
14
|
+
--line: rgba(24, 24, 27, .12); --accent: #047857; --accent-soft: rgba(4, 120, 87, .12);
|
|
15
|
+
--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
|
+
}
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
--bg: #09090b; --panel: #101014; --ink: #f4f4f5; --dim: #a1a1aa;
|
|
23
|
+
--line: rgba(255, 255, 255, .09); --accent: #34d399; --accent-soft: rgba(52, 211, 153, .1);
|
|
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
|
+
}
|
|
29
|
+
}
|
|
30
|
+
body { background: var(--bg); color: var(--ink); }
|
|
31
|
+
.bg-blueprint {
|
|
32
|
+
background-image: linear-gradient(var(--grid) 1px, transparent 1px),
|
|
33
|
+
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
|
34
|
+
background-size: 44px 44px;
|
|
35
|
+
mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 30%, transparent 75%);
|
|
36
|
+
}
|
|
37
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
|
38
|
+
.rowline { border-bottom: 1px solid var(--line); }
|
|
39
|
+
.bar-fill { background: var(--accent); border-radius: 999px; transform-origin: left; }
|
|
40
|
+
.bar-track { background: var(--track); border-radius: 999px; }
|
|
41
|
+
.bar-dim { background: var(--dim); opacity: .55; border-radius: 999px; transform-origin: bottom; }
|
|
42
|
+
.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
|
+
@keyframes ping { 75%, 100% { box-shadow: 0 0 0 9px transparent; } }
|
|
44
|
+
.rise { opacity: 0; transform: translateY(26px); }
|
|
45
|
+
.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
|
+
.tilt { transition: transform .25s cubic-bezier(.16, 1, .3, 1), background .25s; }
|
|
51
|
+
.tilt:hover { transform: translateX(6px); background: var(--accent-soft); }
|
|
52
|
+
.btn-push { transition: transform .12s, background .2s; }
|
|
53
|
+
.btn-push:hover { background: var(--accent-soft); }
|
|
54
|
+
.btn-push:active { transform: scale(.96); }
|
|
55
|
+
a.social { color: var(--ink); }
|
|
56
|
+
a.social:hover { color: var(--accent); }
|
|
57
|
+
.cell { width: 11px; height: 11px; border-radius: 2.5px; background: var(--cell-0); }
|
|
58
|
+
.cell.l1 { background: var(--cell-1); } .cell.l2 { background: var(--cell-2); }
|
|
59
|
+
.cell.l3 { background: var(--cell-3); } .cell.l4 { background: var(--cell-4); }
|
|
60
|
+
.dock {
|
|
61
|
+
position: sticky; top: 12px; z-index: 50;
|
|
62
|
+
background: color-mix(in srgb, var(--panel) 84%, transparent);
|
|
63
|
+
-webkit-backdrop-filter: blur(18px) saturate(160%); backdrop-filter: blur(18px) saturate(160%);
|
|
64
|
+
border: 1px solid var(--line); border-radius: 20px;
|
|
65
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, .25);
|
|
66
|
+
}
|
|
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
|
+
.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
|
+
@media (prefers-reduced-motion: reduce) {
|
|
76
|
+
.rise { opacity: 1; transform: none; }
|
|
77
|
+
.grow, .growY { transform: none; transition: none; }
|
|
78
|
+
.live-dot { animation: none; }
|
|
79
|
+
.tilt, .btn-push { transition: none; }
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body class="min-h-[100dvh] antialiased">
|
|
84
|
+
<div class="bg-blueprint pointer-events-none fixed inset-0" aria-hidden="true"></div>
|
|
85
|
+
|
|
86
|
+
<div class="relative mx-auto max-w-7xl px-4 sm:px-6 pb-16">
|
|
87
|
+
|
|
88
|
+
<!-- Floating dock navbar -->
|
|
89
|
+
<header class="dock mt-4 flex w-full items-center gap-2 px-3 py-2">
|
|
90
|
+
<a href="#" class="flex shrink-0 items-center gap-2.5 pl-1 pr-2" aria-label="Tersio gain home">
|
|
91
|
+
<img src="brand.webp" alt="Tersio" width="32" height="32" class="brand size-8 rounded-full">
|
|
92
|
+
<span class="leading-tight text-left">
|
|
93
|
+
<span class="block font-bold tracking-tight text-sm">TERSIO GAIN</span>
|
|
94
|
+
<span class="mono block text-[11px]" style="color: var(--dim)" id="meta">connecting to ledger</span>
|
|
95
|
+
</span>
|
|
96
|
+
</a>
|
|
97
|
+
<span class="mono hidden shrink-0 sm:flex items-center gap-2 text-xs px-2 ml-auto" style="color: var(--dim)">
|
|
98
|
+
<span class="live-dot inline-block size-2 rounded-full"></span>
|
|
99
|
+
<span id="clock">--:--</span>
|
|
100
|
+
</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">
|
|
102
|
+
<i data-lucide="refresh-cw" class="size-3.5"></i><span class="hidden md:inline">Reload</span>
|
|
103
|
+
</button>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
<!-- Hero: asymmetric 7/5 -->
|
|
107
|
+
<section class="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-10 pb-8 items-end">
|
|
108
|
+
<div class="lg:col-span-7 rise">
|
|
109
|
+
<p class="mono text-[11px] uppercase tracking-[0.22em] mb-3" style="color: var(--accent)">Live token feed</p>
|
|
110
|
+
<p class="mono font-bold tracking-tighter leading-none text-7xl md:text-8xl" id="total">0</p>
|
|
111
|
+
<p class="mt-3 text-base max-w-[52ch]" style="color: var(--dim)">session tokens parsed from OMP transcripts. Input, output, cache read, cache write, USD cost, CO2.</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="lg:col-span-5 rise" style="transition-delay: .12s">
|
|
114
|
+
<dl class="mono text-sm">
|
|
115
|
+
<div class="flex justify-between gap-4 py-2 rowline"><dt style="color: var(--dim)">usd cost</dt><dd id="usd">-</dd></div>
|
|
116
|
+
<div class="flex justify-between gap-4 py-2 rowline"><dt style="color: var(--dim)">co2</dt><dd id="co2">-</dd></div>
|
|
117
|
+
<div class="flex justify-between gap-4 py-2"><dt style="color: var(--dim)">assistant msgs</dt><dd id="msgs">0</dd></div>
|
|
118
|
+
</dl>
|
|
119
|
+
</div>
|
|
120
|
+
</section>
|
|
121
|
+
|
|
122
|
+
<!-- Token strip: plain stats, dividers, no cards -->
|
|
123
|
+
<section class="rise grid grid-cols-2 sm:grid-cols-4 rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" id="tokens" aria-label="Tokens by bucket"></section>
|
|
124
|
+
|
|
125
|
+
<!-- Contribution graph, GitHub style -->
|
|
126
|
+
<section id="activity" class="rise mt-8 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Activity graph">
|
|
127
|
+
<div class="flex items-center gap-2 mb-1">
|
|
128
|
+
<i data-lucide="calendar-days" class="size-4" style="color: var(--accent)"></i>
|
|
129
|
+
<h2 class="font-bold tracking-tight text-xl">Activity</h2>
|
|
130
|
+
<span class="mono text-xs ml-auto" style="color: var(--dim)" id="graphRange"></span>
|
|
131
|
+
</div>
|
|
132
|
+
<p class="mono text-xs mb-4" style="color: var(--dim)">daily session tokens / last 26 weeks / darker means more</p>
|
|
133
|
+
<div class="overflow-x-auto pb-1">
|
|
134
|
+
<div class="flex gap-[3px] w-max" id="graph"></div>
|
|
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>
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<!-- Main: 8/4 split -->
|
|
144
|
+
<main class="grid grid-cols-1 lg:grid-cols-12 gap-8 mt-8">
|
|
145
|
+
|
|
146
|
+
<div class="lg:col-span-8 flex flex-col gap-8">
|
|
147
|
+
<section id="daily" class="rise" style="scroll-margin-top: 90px" aria-label="Daily chart">
|
|
148
|
+
<div class="flex items-center gap-2 mb-1">
|
|
149
|
+
<i data-lucide="chart-column" class="size-4" style="color: var(--accent)"></i>
|
|
150
|
+
<h2 class="font-bold tracking-tight text-xl">Daily input vs output</h2>
|
|
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>
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
158
|
+
|
|
159
|
+
<section id="commands" class="rise" style="scroll-margin-top: 90px" aria-label="Top commands">
|
|
160
|
+
<div class="flex items-center gap-2 mb-1">
|
|
161
|
+
<i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
|
|
162
|
+
<h2 class="font-bold tracking-tight text-xl">Top commands</h2>
|
|
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>
|
|
187
|
+
</div>
|
|
188
|
+
<ol id="models" class="mono text-[13px] space-y-2" style="scroll-margin-top: 90px"></ol>
|
|
189
|
+
</section>
|
|
190
|
+
</div>
|
|
191
|
+
</main>
|
|
192
|
+
|
|
193
|
+
<footer class="mono text-xs mt-12 pt-4 flex items-center gap-4" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
194
|
+
<span>tersio gain dashboard / single file</span>
|
|
195
|
+
<span class="ml-auto flex items-center gap-1" style="color: var(--ink)">
|
|
196
|
+
<a href="https://github.com/KurutoDenzeru" target="_blank" rel="noopener" aria-label="GitHub" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a>
|
|
197
|
+
<a href="https://linkedin.com/in/kurtcalacday/" target="_blank" rel="noopener" aria-label="LinkedIn" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a>
|
|
198
|
+
<a href="https://instagram.com/krtclcdy/" target="_blank" rel="noopener" aria-label="Instagram" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/></svg></a>
|
|
199
|
+
</span>
|
|
200
|
+
<a id="rawlink" href="data.json" class="underline underline-offset-2">raw data.json</a>
|
|
201
|
+
</footer>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<script>
|
|
205
|
+
(function () {
|
|
206
|
+
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
207
|
+
|
|
208
|
+
function fmt(n) { return Number(n).toLocaleString('en-US'); }
|
|
209
|
+
|
|
210
|
+
function countUp(el, target, money) {
|
|
211
|
+
function text(v) { return money ? '$' + v.toFixed(2) : fmt(Math.round(v)); }
|
|
212
|
+
if (reduce || target <= 0) { el.textContent = text(target); return; }
|
|
213
|
+
var t0 = null, dur = 900;
|
|
214
|
+
function step(t) {
|
|
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);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function tick() {
|
|
224
|
+
var c = document.getElementById('clock');
|
|
225
|
+
if (c) c.textContent = new Date().toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
226
|
+
}
|
|
227
|
+
tick(); setInterval(tick, 1000);
|
|
228
|
+
|
|
229
|
+
function dayKey(d) {
|
|
230
|
+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderGraph(byDay) {
|
|
234
|
+
var g = document.getElementById('graph');
|
|
235
|
+
g.innerHTML = '';
|
|
236
|
+
var today = new Date(); today.setHours(0, 0, 0, 0);
|
|
237
|
+
var start = new Date(today); start.setDate(start.getDate() - (26 * 7 - 1) - today.getDay());
|
|
238
|
+
var max = 1, sums = {};
|
|
239
|
+
Object.keys(byDay).forEach(function (day) {
|
|
240
|
+
var b = byDay[day];
|
|
241
|
+
sums[day] = b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
242
|
+
if (sums[day] > max) max = sums[day];
|
|
243
|
+
});
|
|
244
|
+
var weeks = 0;
|
|
245
|
+
for (var w = 0; w < 26; w++) {
|
|
246
|
+
var col = document.createElement('div');
|
|
247
|
+
col.className = 'flex flex-col gap-[3px]';
|
|
248
|
+
for (var i = 0; i < 7; i++) {
|
|
249
|
+
var dt = new Date(start); dt.setDate(dt.getDate() + weeks * 7 + i);
|
|
250
|
+
var key = dayKey(dt), v = sums[key] || 0, lvl = 0;
|
|
251
|
+
if (v > 0) lvl = Math.min(4, 1 + Math.floor((v / max) * 3.99));
|
|
252
|
+
var cell = document.createElement('span');
|
|
253
|
+
cell.className = 'cell' + (lvl ? ' l' + lvl : '');
|
|
254
|
+
cell.title = key + ': ' + fmt(v) + ' tokens';
|
|
255
|
+
col.appendChild(cell);
|
|
256
|
+
}
|
|
257
|
+
weeks++;
|
|
258
|
+
g.appendChild(col);
|
|
259
|
+
}
|
|
260
|
+
document.getElementById('graphRange').textContent =
|
|
261
|
+
dayKey(start) + ' to ' + dayKey(today);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderChart(byDay) {
|
|
265
|
+
var chart = document.getElementById('chart'), days = document.getElementById('chartDays');
|
|
266
|
+
chart.innerHTML = ''; days.innerHTML = '';
|
|
267
|
+
var list = [], today = new Date(); today.setHours(0, 0, 0, 0);
|
|
268
|
+
for (var i = 13; i >= 0; i--) {
|
|
269
|
+
var dt = new Date(today); dt.setDate(dt.getDate() - i);
|
|
270
|
+
list.push({ key: dayKey(dt), label: String(dt.getDate()), b: byDay[dayKey(dt)] || { input: 0, output: 0 } });
|
|
271
|
+
}
|
|
272
|
+
var max = 1;
|
|
273
|
+
list.forEach(function (d) { max = Math.max(max, d.b.input, d.b.output); });
|
|
274
|
+
list.forEach(function (d, i) {
|
|
275
|
+
var wrap = document.createElement('div');
|
|
276
|
+
wrap.className = 'flex-1 flex items-end justify-center gap-1 h-full';
|
|
277
|
+
wrap.title = d.key + ': in ' + fmt(d.b.input) + ' / out ' + fmt(d.b.output);
|
|
278
|
+
var bi = document.createElement('div');
|
|
279
|
+
bi.className = 'bar-fill growY w-3';
|
|
280
|
+
bi.style.height = Math.max(2, Math.round(d.b.input / max * 100)) + '%';
|
|
281
|
+
bi.style.transitionDelay = reduce ? '' : (i * 40) + 'ms';
|
|
282
|
+
var bo = document.createElement('div');
|
|
283
|
+
bo.className = 'bar-dim growY w-3';
|
|
284
|
+
bo.style.height = Math.max(2, Math.round(d.b.output / max * 100)) + '%';
|
|
285
|
+
bo.style.transitionDelay = reduce ? '' : (i * 40 + 60) + 'ms';
|
|
286
|
+
wrap.appendChild(bi); wrap.appendChild(bo); chart.appendChild(wrap);
|
|
287
|
+
var lb = document.createElement('span');
|
|
288
|
+
lb.className = 'flex-1 text-center'; lb.textContent = d.label;
|
|
289
|
+
days.appendChild(lb);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function render(d) {
|
|
294
|
+
var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
295
|
+
var total = t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
296
|
+
document.getElementById('meta').textContent =
|
|
297
|
+
fmt(total) + ' session tokens / ' + d.total + ' ledger rows';
|
|
298
|
+
countUp(document.getElementById('total'), total);
|
|
299
|
+
countUp(document.getElementById('usd'), d.usd || 0, true);
|
|
300
|
+
document.getElementById('co2').textContent = '~' + (d.co2g || 0).toFixed(1) + 'g (est.)';
|
|
301
|
+
document.getElementById('msgs').textContent = fmt(d.messages || 0);
|
|
302
|
+
|
|
303
|
+
var buckets = [['input', t.input, 'arrow-down-to-line'], ['output', t.output, 'arrow-up-from-line'], ['cache read', t.cacheRead, 'hard-drive-download'], ['cache write', t.cacheWrite, 'hard-drive-upload']];
|
|
304
|
+
var strip = document.getElementById('tokens');
|
|
305
|
+
strip.innerHTML = '';
|
|
306
|
+
buckets.forEach(function (b, i) {
|
|
307
|
+
var cell = document.createElement('div');
|
|
308
|
+
cell.className = 'tilt px-4 py-4' + (i > 0 ? ' sm:border-l' : '');
|
|
309
|
+
cell.style.borderColor = 'var(--line)';
|
|
310
|
+
cell.innerHTML = '<div class="flex items-center gap-2 mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">' +
|
|
311
|
+
'<i data-lucide="' + b[2] + '" class="size-3.5"></i><span></span></div>' +
|
|
312
|
+
'<p class="mono font-bold text-3xl mt-1"></p>';
|
|
313
|
+
cell.querySelector('span').textContent = b[0];
|
|
314
|
+
cell.querySelector('p').textContent = fmt(b[1]);
|
|
315
|
+
strip.appendChild(cell);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
renderGraph(d.byDay || {});
|
|
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.';
|
|
328
|
+
|
|
329
|
+
var models = document.getElementById('models');
|
|
330
|
+
models.innerHTML = '';
|
|
331
|
+
Object.entries(d.byModel || {}).sort(function (a, b) {
|
|
332
|
+
return (b[1].input + b[1].output) - (a[1].input + a[1].output);
|
|
333
|
+
}).slice(0, 5).forEach(function (row) {
|
|
334
|
+
var li = document.createElement('li');
|
|
335
|
+
li.className = 'flex justify-between gap-2';
|
|
336
|
+
var name = document.createElement('span'); name.className = 'truncate'; name.textContent = row[0];
|
|
337
|
+
var n = document.createElement('span'); n.textContent = fmt(row[1].input + row[1].output);
|
|
338
|
+
li.appendChild(name); li.appendChild(n); models.appendChild(li);
|
|
339
|
+
});
|
|
340
|
+
if (!models.children.length) {
|
|
341
|
+
models.innerHTML = '<li style="color: var(--dim)">no session tokens yet</li>';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
var ol = document.getElementById('top');
|
|
345
|
+
ol.innerHTML = '';
|
|
346
|
+
var max = 1;
|
|
347
|
+
(d.byDetail || []).forEach(function (row) { if (row[1] > max) max = row[1]; });
|
|
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);
|
|
366
|
+
|
|
367
|
+
if (window.lucide) lucide.createIcons();
|
|
368
|
+
observe();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
var seen = new WeakSet();
|
|
372
|
+
function observe() {
|
|
373
|
+
var els = document.querySelectorAll('.rise, .grow, .growY');
|
|
374
|
+
if (!('IntersectionObserver' in window) || reduce) {
|
|
375
|
+
els.forEach(function (el) { el.classList.add('in'); });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
var io = new IntersectionObserver(function (entries) {
|
|
379
|
+
entries.forEach(function (e) {
|
|
380
|
+
if (e.isIntersecting && !seen.has(e.target)) {
|
|
381
|
+
seen.add(e.target); e.target.classList.add('in'); io.unobserve(e.target);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}, { threshold: 0.12 });
|
|
385
|
+
els.forEach(function (el) { if (!seen.has(el)) io.observe(el); });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function load() {
|
|
389
|
+
document.getElementById('meta').textContent = 'connecting to ledger';
|
|
390
|
+
fetch('data.json').then(function (r) { return r.json(); }).then(render).catch(function () {
|
|
391
|
+
document.getElementById('meta').textContent = 'No live data. Serve with: tersio dashboard';
|
|
392
|
+
document.getElementById('rawlink').style.display = 'none';
|
|
393
|
+
document.getElementById('emptyTop').classList.remove('hidden');
|
|
394
|
+
if (window.lucide) lucide.createIcons();
|
|
395
|
+
observe();
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
document.getElementById('reload').addEventListener('click', load);
|
|
399
|
+
load();
|
|
400
|
+
observe();
|
|
401
|
+
})();
|
|
402
|
+
</script>
|
|
403
|
+
</body>
|
|
404
|
+
</html>
|
package/cli/dashboard.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// cli/dashboard.ts — localhost gain dashboard. Serves the single plain-HTML
|
|
2
|
+
// template (no deps, no build) plus a /data.json endpoint from the ledger.
|
|
3
|
+
// Binds 127.0.0.1 only; --export writes a file://-ready file instead.
|
|
4
|
+
// Note: no Promise.withResolvers here — engines still allow Node 20.12,
|
|
5
|
+
// which lacks it; the listening server itself keeps the process alive.
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { readUsage } from '../extensions/shared/usage-ledger.js';
|
|
12
|
+
import { summarizeUsage } from './usage.js';
|
|
13
|
+
import { withInteractiveSpinner } from './interactive.js';
|
|
14
|
+
const TEMPLATE = path.join(path.dirname(fileURLToPath(import.meta.url)), 'dashboard.html');
|
|
15
|
+
const BRAND = path.join(path.dirname(fileURLToPath(import.meta.url)), 'brand.webp');
|
|
16
|
+
async function templateHtml() {
|
|
17
|
+
return fs.readFile(TEMPLATE, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
async function faviconDataUri() {
|
|
20
|
+
const png = await fs.readFile(BRAND);
|
|
21
|
+
return `data:image/webp;base64,${png.toString('base64')}`;
|
|
22
|
+
}
|
|
23
|
+
function dataJson() {
|
|
24
|
+
return JSON.stringify(summarizeUsage(readUsage()));
|
|
25
|
+
}
|
|
26
|
+
function openBrowser(url) {
|
|
27
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
28
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
29
|
+
}
|
|
30
|
+
async function runDashboard(options) {
|
|
31
|
+
if (options.exportFile) {
|
|
32
|
+
const html = await templateHtml();
|
|
33
|
+
const inline = html
|
|
34
|
+
.replace("fetch('data.json')", `Promise.resolve({ json: function () { return ${dataJson()}; } }).then(function (r) { return r.json(); }).then`)
|
|
35
|
+
.replace('href="brand.webp"', `href="${await faviconDataUri()}"`)
|
|
36
|
+
.replace('src="brand.webp"', `src="${await faviconDataUri()}"`);
|
|
37
|
+
await fs.writeFile(options.exportFile, inline, 'utf8');
|
|
38
|
+
console.log(`[ok] dashboard exported → ${options.exportFile}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const html = await withInteractiveSpinner('Loading dashboard template', templateHtml);
|
|
42
|
+
const server = http.createServer(async (req, res) => {
|
|
43
|
+
if (req.url === '/data.json') {
|
|
44
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
45
|
+
res.end(dataJson());
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (req.url === '/brand.webp') {
|
|
49
|
+
try {
|
|
50
|
+
const png = await fs.readFile(BRAND);
|
|
51
|
+
res.writeHead(200, { 'Content-Type': 'image/webp' });
|
|
52
|
+
res.end(png);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
res.writeHead(404);
|
|
56
|
+
res.end();
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
61
|
+
res.end(html);
|
|
62
|
+
});
|
|
63
|
+
server.listen(options.port, '127.0.0.1', () => {
|
|
64
|
+
const address = server.address();
|
|
65
|
+
const port = typeof address === 'object' && address ? address.port : options.port;
|
|
66
|
+
const url = `http://127.0.0.1:${port}`;
|
|
67
|
+
console.log(`[ok] dashboard live → ${url} (Ctrl-C to stop)`);
|
|
68
|
+
if (options.open)
|
|
69
|
+
openBrowser(url);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export { runDashboard };
|
package/cli/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
5
|
import { runInteractivePhase } from './interactive.js';
|
|
6
|
+
import { ledgerPath, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
6
7
|
import { checkForUpdate } from './update.js';
|
|
7
8
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
9
|
async function runDoctor() {
|
|
@@ -123,6 +124,9 @@ async function runDoctor() {
|
|
|
123
124
|
check('Self plugin package', selfPkgText !== null, parseJsonObject(selfPkgText)?.version ?? '');
|
|
124
125
|
const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
|
|
125
126
|
check('Self plugin in plugins/package.json', selfDep);
|
|
127
|
+
section('Usage');
|
|
128
|
+
const usageRows = readUsage();
|
|
129
|
+
check('Usage ledger', usageRows.length > 0, usageRows.length ? `${usageRows.length} rows · ${ledgerPath()}` : ledgerPath());
|
|
126
130
|
section('Add-ons');
|
|
127
131
|
const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
|
|
128
132
|
check('Caveman rule', cavemanRuleText !== null, ruleAge);
|
package/cli/usage.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
+
import { co2Grams, importSessionTokens, readUsage, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
|
+
import { withInteractiveSpinner } from './interactive.js';
|
|
4
|
+
export function summarizeUsage(rows) {
|
|
5
|
+
const byKind = {};
|
|
6
|
+
const detailCounts = {};
|
|
7
|
+
for (const r of rows) {
|
|
8
|
+
byKind[r.kind] = (byKind[r.kind] ?? 0) + 1;
|
|
9
|
+
detailCounts[r.detail] = (detailCounts[r.detail] ?? 0) + 1;
|
|
10
|
+
}
|
|
11
|
+
const byDetail = Object.entries(detailCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
12
|
+
const session = importSessionTokens();
|
|
13
|
+
let usd = 0;
|
|
14
|
+
let priced = true;
|
|
15
|
+
for (const [model, t] of Object.entries(session.byModel)) {
|
|
16
|
+
const c = usdCost(t, model);
|
|
17
|
+
usd += c.usd;
|
|
18
|
+
if (!c.priced)
|
|
19
|
+
priced = false;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
total: rows.length,
|
|
23
|
+
byKind,
|
|
24
|
+
byDetail,
|
|
25
|
+
lastWrite: rows.length ? new Date(rows[rows.length - 1].ts).toISOString() : null,
|
|
26
|
+
empty: rows.length === 0 && session.messages === 0,
|
|
27
|
+
messages: session.messages,
|
|
28
|
+
tokens: session.totals,
|
|
29
|
+
byModel: session.byModel,
|
|
30
|
+
byDay: session.byDay,
|
|
31
|
+
usd,
|
|
32
|
+
priced,
|
|
33
|
+
co2g: co2Grams(session.totals.output),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function fmt(n) {
|
|
37
|
+
return n.toLocaleString('en-US');
|
|
38
|
+
}
|
|
39
|
+
function printReport(report) {
|
|
40
|
+
console.log('\n=== Tersio Usage ===');
|
|
41
|
+
if (report.empty) {
|
|
42
|
+
console.log(' No ledger rows or session tokens yet — run session commands first.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const t = report.tokens;
|
|
46
|
+
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.)`);
|
|
48
|
+
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
|
+
if (models.length) {
|
|
50
|
+
console.log(' By model');
|
|
51
|
+
for (const [model, b] of models) {
|
|
52
|
+
console.log(` ${model}: in ${fmt(b.input)} · out ${fmt(b.output)} · cache r/w ${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (report.total) {
|
|
56
|
+
console.log(' Activity');
|
|
57
|
+
for (const [kind, n] of Object.entries(report.byKind)) {
|
|
58
|
+
console.log(` ${kind}: ${n}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(' Top commands');
|
|
61
|
+
for (const [detail, n] of report.byDetail) {
|
|
62
|
+
console.log(` ${n}× ${detail}`);
|
|
63
|
+
}
|
|
64
|
+
console.log(` Total rows: ${report.total} · last write: ${report.lastWrite}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function runUsage() {
|
|
68
|
+
const report = await withInteractiveSpinner('Reading usage ledger', async () => summarizeUsage(readUsage()));
|
|
69
|
+
printReport(report);
|
|
70
|
+
}
|
|
71
|
+
export { runUsage };
|
|
@@ -82,6 +82,33 @@ function checkCaveman() {
|
|
|
82
82
|
return `Caveman ${status}: local=${localHash || '—'} remote=${remoteHash}`;
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
|
+
export async function checkAddonsSummary(ctx) {
|
|
86
|
+
return checkAddons(ctx);
|
|
87
|
+
}
|
|
88
|
+
export async function runAddonUpdate(pi, ctx, target, dryRun = false) {
|
|
89
|
+
const results = [];
|
|
90
|
+
const updaters = {
|
|
91
|
+
ponytail: () => updatePonytail(pi, ctx, dryRun),
|
|
92
|
+
rtk: () => updateRtk(ctx, dryRun),
|
|
93
|
+
caveman: () => updateCaveman(ctx, dryRun),
|
|
94
|
+
};
|
|
95
|
+
if (target === 'all') {
|
|
96
|
+
notify(ctx, `ai-addons update all${dryRun ? ' dry-run' : ''}: starting ponytail → rtk → caveman sequentially…`, 'info');
|
|
97
|
+
for (const name of ['ponytail', 'rtk', 'caveman'])
|
|
98
|
+
results.push(await updaters[name]());
|
|
99
|
+
if (!dryRun)
|
|
100
|
+
results.push(RELOAD_MSG);
|
|
101
|
+
notify(ctx, `ai-addons update all ${dryRun ? 'dry-run ' : ''}complete.${dryRun ? '' : ` ${RELOAD_MSG}`}`, 'info');
|
|
102
|
+
}
|
|
103
|
+
else if (Object.hasOwn(updaters, target)) {
|
|
104
|
+
results.push(await updaters[target]());
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const m = 'Usage: /ai-addons update <ponytail|rtk|caveman|all> [--dry-run]';
|
|
108
|
+
return report(ctx, m, 'warning');
|
|
109
|
+
}
|
|
110
|
+
return results.join('\n\n');
|
|
111
|
+
}
|
|
85
112
|
async function checkAddons(ctx) {
|
|
86
113
|
const [ponytail, rtk, caveman] = await Promise.all([checkPonytail(), checkRtk(), checkCaveman()]);
|
|
87
114
|
const lines = [];
|
|
@@ -294,29 +321,7 @@ export default function aiAddonsUpdaterExtension(pi) {
|
|
|
294
321
|
return summary;
|
|
295
322
|
}
|
|
296
323
|
if (sub === 'update' && cleanParts[1]) {
|
|
297
|
-
|
|
298
|
-
const results = [];
|
|
299
|
-
const updaters = {
|
|
300
|
-
ponytail: () => updatePonytail(pi, ctx, dryRun),
|
|
301
|
-
rtk: () => updateRtk(ctx, dryRun),
|
|
302
|
-
caveman: () => updateCaveman(ctx, dryRun),
|
|
303
|
-
};
|
|
304
|
-
if (target === 'all') {
|
|
305
|
-
notify(ctx, `ai-addons update all${dryRun ? ' dry-run' : ''}: starting ponytail → rtk → caveman sequentially…`, 'info');
|
|
306
|
-
for (const name of ['ponytail', 'rtk', 'caveman'])
|
|
307
|
-
results.push(await updaters[name]());
|
|
308
|
-
if (!dryRun)
|
|
309
|
-
results.push(RELOAD_MSG);
|
|
310
|
-
notify(ctx, `ai-addons update all ${dryRun ? 'dry-run ' : ''}complete.${dryRun ? '' : ` ${RELOAD_MSG}`}`, 'info');
|
|
311
|
-
}
|
|
312
|
-
else if (Object.hasOwn(updaters, target)) {
|
|
313
|
-
results.push(await updaters[target]());
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
const m = 'Usage: /ai-addons update <ponytail|rtk|caveman|all> [--dry-run]';
|
|
317
|
-
return report(ctx, m, 'warning');
|
|
318
|
-
}
|
|
319
|
-
return results.join('\n\n');
|
|
324
|
+
return runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
|
|
320
325
|
}
|
|
321
326
|
const m = 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]';
|
|
322
327
|
return report(ctx, m, 'warning');
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Shared usage ledger: append-only JSON-lines file. One source of truth read
|
|
2
|
+
// by `tersio usage`, `/tersio usage`, and the gain dashboard.
|
|
3
|
+
// Best-effort by design: a ledger failure never breaks the caller, and
|
|
4
|
+
// corrupt lines are skipped on read (same pattern as the config normalizer).
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
export function ledgerPath() {
|
|
9
|
+
const override = process.env.TERSIO_USAGE_FILE;
|
|
10
|
+
if (override)
|
|
11
|
+
return override;
|
|
12
|
+
return path.join(os.homedir(), '.omp', 'plugins', 'tersio-usage.jsonl');
|
|
13
|
+
}
|
|
14
|
+
export function appendUsage(kind, detail) {
|
|
15
|
+
const row = JSON.stringify({ ts: Date.now(), kind, detail }) + '\n';
|
|
16
|
+
try {
|
|
17
|
+
fs.mkdirSync(path.dirname(ledgerPath()), { recursive: true });
|
|
18
|
+
fs.appendFileSync(ledgerPath(), row, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
catch { /* ledger is best-effort; never break the caller */ }
|
|
21
|
+
}
|
|
22
|
+
function zeroBreakdown() {
|
|
23
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
24
|
+
}
|
|
25
|
+
function addInto(into, u) {
|
|
26
|
+
for (const k of ['input', 'output', 'cacheRead', 'cacheWrite']) {
|
|
27
|
+
const v = u[k];
|
|
28
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0)
|
|
29
|
+
into[k] += Math.floor(v);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function sessionsDir() {
|
|
33
|
+
const override = process.env.TERSIO_SESSIONS_DIR;
|
|
34
|
+
if (override)
|
|
35
|
+
return override;
|
|
36
|
+
return path.join(os.homedir(), '.omp', 'agent', 'sessions');
|
|
37
|
+
}
|
|
38
|
+
function dayKey(ts) {
|
|
39
|
+
const ms = typeof ts === 'number' ? ts : Date.parse(ts);
|
|
40
|
+
if (!Number.isFinite(ms))
|
|
41
|
+
return null;
|
|
42
|
+
const d = new Date(ms);
|
|
43
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
44
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
45
|
+
}
|
|
46
|
+
function walkJsonl(dir, out, cap) {
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const e of entries) {
|
|
55
|
+
if (out.length >= cap)
|
|
56
|
+
return;
|
|
57
|
+
const full = path.join(dir, e.name);
|
|
58
|
+
if (e.isDirectory())
|
|
59
|
+
walkJsonl(full, out, cap);
|
|
60
|
+
else if (e.isFile() && e.name.endsWith('.jsonl'))
|
|
61
|
+
out.push(full);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function importSessionTokens() {
|
|
65
|
+
const byModel = {};
|
|
66
|
+
const byDay = {};
|
|
67
|
+
const totals = zeroBreakdown();
|
|
68
|
+
let messages = 0;
|
|
69
|
+
const files = [];
|
|
70
|
+
walkJsonl(sessionsDir(), files, 2000);
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
let text;
|
|
73
|
+
try {
|
|
74
|
+
text = fs.readFileSync(file, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const line of text.split('\n')) {
|
|
80
|
+
if (!line.trim())
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const row = JSON.parse(line);
|
|
84
|
+
const msg = row.message;
|
|
85
|
+
if (!msg || msg.role !== 'assistant' || !msg.usage)
|
|
86
|
+
continue;
|
|
87
|
+
const model = typeof msg.model === 'string' && msg.model ? msg.model : 'unknown';
|
|
88
|
+
addInto(totals, msg.usage);
|
|
89
|
+
byModel[model] ??= zeroBreakdown();
|
|
90
|
+
addInto(byModel[model], msg.usage);
|
|
91
|
+
const day = row.timestamp !== undefined ? dayKey(row.timestamp) : null;
|
|
92
|
+
if (day) {
|
|
93
|
+
byDay[day] ??= zeroBreakdown();
|
|
94
|
+
addInto(byDay[day], msg.usage);
|
|
95
|
+
}
|
|
96
|
+
messages += 1;
|
|
97
|
+
}
|
|
98
|
+
catch { /* skip corrupt lines */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { messages, totals, byModel, byDay };
|
|
102
|
+
}
|
|
103
|
+
const PRICE_TABLE = [
|
|
104
|
+
{ match: ':free', price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
|
|
105
|
+
{ match: 'opus', price: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 } },
|
|
106
|
+
{ match: 'sonnet', price: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } },
|
|
107
|
+
{ match: 'haiku', price: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 } },
|
|
108
|
+
{ match: 'gpt', price: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 2.5 } },
|
|
109
|
+
{ match: 'gemini', price: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 1.25 } },
|
|
110
|
+
{ match: 'glm', price: { input: 0.5, output: 1, cacheRead: 0.05, cacheWrite: 0.5 } },
|
|
111
|
+
];
|
|
112
|
+
const DEFAULT_PRICE = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
113
|
+
export function priceFor(model) {
|
|
114
|
+
const name = model.toLowerCase();
|
|
115
|
+
for (const row of PRICE_TABLE) {
|
|
116
|
+
if (name.includes(row.match))
|
|
117
|
+
return { price: row.price, known: true };
|
|
118
|
+
}
|
|
119
|
+
return { price: DEFAULT_PRICE, known: false };
|
|
120
|
+
}
|
|
121
|
+
export function usdCost(t, model) {
|
|
122
|
+
const { price, known } = model ? priceFor(model) : { price: DEFAULT_PRICE, known: false };
|
|
123
|
+
const m = 1 / 1_000_000;
|
|
124
|
+
return {
|
|
125
|
+
usd: (t.input * price.input + t.output * price.output + t.cacheRead * price.cacheRead + t.cacheWrite * price.cacheWrite) * m,
|
|
126
|
+
priced: model ? known : false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ponytail: CO2 is a rough server-energy multiple on output tokens, not metered.
|
|
130
|
+
// Always render with ~est. and never merge with measured figures.
|
|
131
|
+
export const CO2_G_PER_1K_OUTPUT = 0.2;
|
|
132
|
+
export function co2Grams(outputTokens) {
|
|
133
|
+
return (outputTokens / 1000) * CO2_G_PER_1K_OUTPUT;
|
|
134
|
+
}
|
|
135
|
+
export function readUsage() {
|
|
136
|
+
let text;
|
|
137
|
+
try {
|
|
138
|
+
text = fs.readFileSync(ledgerPath(), 'utf8');
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const rows = [];
|
|
144
|
+
for (const line of text.split('\n')) {
|
|
145
|
+
if (!line.trim())
|
|
146
|
+
continue;
|
|
147
|
+
try {
|
|
148
|
+
const row = JSON.parse(line);
|
|
149
|
+
if (typeof row.ts === 'number' && typeof row.kind === 'string' && typeof row.detail === 'string') {
|
|
150
|
+
rows.push({ ts: row.ts, kind: row.kind, detail: row.detail });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* skip corrupt lines */ }
|
|
154
|
+
}
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// /tersio unified root command — routes every tersio capability through one
|
|
2
|
+
// namespaced entrypoint. Aliases (/caveman, /rtk, /combo, /ai-addons) keep
|
|
3
|
+
// working: this router drives the same shared bridge + session entries the
|
|
4
|
+
// sibling extensions restore from, then reloads so their local mirrors sync
|
|
5
|
+
// (same mechanism /combo already uses to drive its siblings).
|
|
6
|
+
import { COMBO_LEVELS, activeModesSummary, getSharedComboState, normalizeComboLevel, normalizeMode, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboMode, } from '../shared/session-state.js';
|
|
7
|
+
import { appendUsage, readUsage } from '../shared/usage-ledger.js';
|
|
8
|
+
import { checkAddonsSummary, runAddonUpdate } from '../ai-addons-updater/index.js';
|
|
9
|
+
const HELP = [
|
|
10
|
+
'/tersio caveman lite|full|ultra|wenyan|off — terse-reply mode',
|
|
11
|
+
'/tersio combo off|medium|balanced|max — preset for all three modes',
|
|
12
|
+
'/tersio rtk on|off — compact shell output',
|
|
13
|
+
'/tersio ponytail off|lite|full|ultra|review — minimal code',
|
|
14
|
+
'/tersio status — active modes + combo level',
|
|
15
|
+
'/tersio check — add-on version check',
|
|
16
|
+
'/tersio update <ponytail|rtk|caveman|all> [--dry-run]',
|
|
17
|
+
'/tersio gain — savings summary + dashboard hint',
|
|
18
|
+
'/tersio usage — ledger report for this machine',
|
|
19
|
+
'/tersio help — this table',
|
|
20
|
+
'aliases (still work): /caveman /rtk /combo /ai-addons',
|
|
21
|
+
].join('\n');
|
|
22
|
+
function statusLine() {
|
|
23
|
+
const s = getSharedComboState();
|
|
24
|
+
const level = s.level === 'custom' ? 'INACTIVE' : s.level.toUpperCase();
|
|
25
|
+
return `tersio — caveman=${s.caveman.toUpperCase()} · rtk=${s.rtk.toUpperCase()} · ponytail=${s.ponytail.toUpperCase()} (combo ${level})`;
|
|
26
|
+
}
|
|
27
|
+
function usageSummary() {
|
|
28
|
+
const rows = readUsage();
|
|
29
|
+
if (rows.length === 0)
|
|
30
|
+
return 'tersio usage: no ledger yet — run commands first.';
|
|
31
|
+
const byKind = {};
|
|
32
|
+
for (const r of rows)
|
|
33
|
+
byKind[r.kind] = (byKind[r.kind] ?? 0) + 1;
|
|
34
|
+
const parts = Object.entries(byKind).map(([k, n]) => `${k}×${n}`);
|
|
35
|
+
const last = new Date(rows[rows.length - 1].ts).toISOString();
|
|
36
|
+
return `tersio usage: ${rows.length} rows (${parts.join(', ')}), last write ${last}.`;
|
|
37
|
+
}
|
|
38
|
+
export default function tersioCommandsExtension(pi) {
|
|
39
|
+
pi.setLabel?.('Tersio unified root command');
|
|
40
|
+
async function reload(ctx) {
|
|
41
|
+
if (ctx?.reload)
|
|
42
|
+
await ctx.reload();
|
|
43
|
+
}
|
|
44
|
+
pi.registerCommand?.('tersio', {
|
|
45
|
+
description: 'Tersio root: caveman|combo|rtk|ponytail|status|check|update|gain|usage|help',
|
|
46
|
+
handler: async (args, ctx) => {
|
|
47
|
+
const parts = String(args || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
48
|
+
const [sub, rest] = [parts[0], parts.slice(1).join(' ')];
|
|
49
|
+
appendUsage('command', `/tersio ${parts.join(' ')}`.trim());
|
|
50
|
+
if (!sub || sub === 'status') {
|
|
51
|
+
if (ctx?.hasUI)
|
|
52
|
+
reconcileSharedComboEntries(sessionEntries(ctx));
|
|
53
|
+
ctx?.ui?.notify?.(statusLine(), 'info');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (sub === 'help') {
|
|
57
|
+
ctx?.ui?.notify?.(HELP, 'info');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (sub === 'caveman') {
|
|
61
|
+
const mode = normalizeMode('caveman', rest);
|
|
62
|
+
if (!mode) {
|
|
63
|
+
ctx?.ui?.notify?.('Usage: /tersio caveman [lite|full|ultra|wenyan|off|status]', 'warning');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
pi.appendEntry?.('caveman-mode', { mode });
|
|
67
|
+
setSharedComboMode('caveman', mode);
|
|
68
|
+
appendUsage('toggle', `caveman=${mode}`);
|
|
69
|
+
ctx?.ui?.notify?.(`Caveman ${mode} on — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
70
|
+
await reload(ctx);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (sub === 'rtk') {
|
|
74
|
+
if (!rest || rest === 'status') {
|
|
75
|
+
const on = getSharedComboState().rtk === 'on';
|
|
76
|
+
ctx?.ui?.notify?.(`RTK: ${on ? 'on' : 'off'}`, 'info');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!['on', 'off'].includes(rest)) {
|
|
80
|
+
ctx?.ui?.notify?.('Usage: /tersio rtk [on|off|status]', 'warning');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const enabled = rest === 'on';
|
|
84
|
+
pi.appendEntry?.('rtk-mode', { enabled });
|
|
85
|
+
setSharedComboMode('rtk', enabled);
|
|
86
|
+
appendUsage('toggle', `rtk=${rest}`);
|
|
87
|
+
ctx?.ui?.notify?.(`RTK ${rest} — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
88
|
+
await reload(ctx);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (sub === 'ponytail') {
|
|
92
|
+
const mode = normalizeMode('ponytail', rest);
|
|
93
|
+
if (!mode) {
|
|
94
|
+
ctx?.ui?.notify?.('Usage: /tersio ponytail [off|lite|full|ultra|review|status]', 'warning');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
pi.appendEntry?.('ponytail-mode', { mode });
|
|
98
|
+
setSharedComboMode('ponytail', mode);
|
|
99
|
+
appendUsage('toggle', `ponytail=${mode}`);
|
|
100
|
+
ctx?.ui?.notify?.(`Ponytail ${mode} — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
101
|
+
await reload(ctx);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (sub === 'combo') {
|
|
105
|
+
if (!rest || rest === 'status') {
|
|
106
|
+
if (ctx?.hasUI)
|
|
107
|
+
reconcileSharedComboEntries(sessionEntries(ctx));
|
|
108
|
+
ctx?.ui?.notify?.(statusLine(), 'info');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const level = normalizeComboLevel(rest);
|
|
112
|
+
if (!level) {
|
|
113
|
+
ctx?.ui?.notify?.('Unknown combo level. Use: off | medium | balanced | max', 'warning');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const modes = COMBO_LEVELS[level];
|
|
117
|
+
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
118
|
+
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
119
|
+
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
120
|
+
pi.appendEntry?.('combo-level', { level });
|
|
121
|
+
setSharedComboLevel(level);
|
|
122
|
+
appendUsage('toggle', `combo=${level}`);
|
|
123
|
+
ctx?.ui?.notify?.(`Combo ${level} on — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
124
|
+
await reload(ctx);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (sub === 'check') {
|
|
128
|
+
await checkAddonsSummary(ctx);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (sub === 'update') {
|
|
132
|
+
const dryRun = parts.includes('--dry-run');
|
|
133
|
+
const target = parts.filter((p) => p !== '--dry-run' && p !== 'update' && p !== 'tersio').join(' ');
|
|
134
|
+
if (!target) {
|
|
135
|
+
ctx?.ui?.notify?.('Usage: /tersio update <ponytail|rtk|caveman|all> [--dry-run]', 'warning');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await runAddonUpdate(pi, ctx, target, dryRun);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (sub === 'gain' || sub === 'usage') {
|
|
142
|
+
ctx?.ui?.notify?.(`${usageSummary()}\nFull dashboard: run \`tersio dashboard --open\` in a shell.`, 'info');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
ctx?.ui?.notify?.(`Unknown subcommand: ${sub}.\n${HELP}`, 'warning');
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "✂️ Token-saving OMP add-ons: terse Caveman replies, compact RTK shell output, lean Ponytail code calls — one-command combo presets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"omp",
|
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
"extensions": [
|
|
61
61
|
"./extensions/ai-addons-updater/index.js"
|
|
62
62
|
]
|
|
63
|
+
},
|
|
64
|
+
"tersio": {
|
|
65
|
+
"description": "Unified /tersio root command (caveman, combo, rtk, ponytail, status, check, update, gain, usage)",
|
|
66
|
+
"default": true,
|
|
67
|
+
"extensions": [
|
|
68
|
+
"./extensions/tersio-commands/index.js"
|
|
69
|
+
]
|
|
63
70
|
}
|
|
64
71
|
},
|
|
65
72
|
"settings": {
|
|
@@ -108,6 +115,8 @@
|
|
|
108
115
|
"files": [
|
|
109
116
|
"tersio.js",
|
|
110
117
|
"cli/**/*.js",
|
|
118
|
+
"cli/dashboard.html",
|
|
119
|
+
"cli/brand.webp",
|
|
111
120
|
"extensions/**/*.js"
|
|
112
121
|
],
|
|
113
122
|
"scripts": {
|
package/tersio.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
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, doctor, showHelp, showVersion, uninstall, unknownCommand, update, } from './cli/common.js';
|
|
5
|
+
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, 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';
|
|
9
9
|
import { runLatestUpdate } from './cli/update.js';
|
|
10
10
|
import { runUninstall } from './cli/uninstall.js';
|
|
11
|
+
import { runUsage } from './cli/usage.js';
|
|
12
|
+
import { runDashboard } from './cli/dashboard.js';
|
|
11
13
|
function printHelp() {
|
|
12
14
|
console.log(`Usage: ${PACKAGE_BIN} [command] [options]
|
|
13
15
|
|
|
@@ -16,6 +18,8 @@ Commands:
|
|
|
16
18
|
update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail)
|
|
17
19
|
reinstall Clean and reinstall the user-scope add-ons
|
|
18
20
|
doctor Check the current installation
|
|
21
|
+
usage Ledger-backed usage + savings report
|
|
22
|
+
dashboard Serve the gain dashboard (localhost only)
|
|
19
23
|
uninstall Remove the managed extensions
|
|
20
24
|
version Print the package version
|
|
21
25
|
help Show this help
|
|
@@ -31,6 +35,9 @@ Options:
|
|
|
31
35
|
--yes, -y
|
|
32
36
|
--dry-run
|
|
33
37
|
--verbose
|
|
38
|
+
--port <n> (dashboard: pin port, default ephemeral)
|
|
39
|
+
--open (dashboard: open browser)
|
|
40
|
+
--export <file> (dashboard: write HTML file instead of serving)
|
|
34
41
|
--version, -v
|
|
35
42
|
--help, -h`);
|
|
36
43
|
}
|
|
@@ -62,6 +69,16 @@ async function main() {
|
|
|
62
69
|
closeRL();
|
|
63
70
|
return;
|
|
64
71
|
}
|
|
72
|
+
if (usage) {
|
|
73
|
+
await runUsage();
|
|
74
|
+
closeRL();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (dashboard) {
|
|
78
|
+
await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport });
|
|
79
|
+
closeRL();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
65
82
|
if (uninstall) {
|
|
66
83
|
await runUninstall();
|
|
67
84
|
closeRL();
|