@krtclcdy/tersio 2.22.0 → 2.23.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 +38 -30
- package/cli/common.js +43 -39
- package/cli/currency.js +2 -2
- package/cli/dashboard.js +60 -116
- package/cli/doctor-fix.js +35 -29
- package/cli/doctor.js +26 -15
- package/cli/install.js +15 -34
- package/cli/profile.js +2 -1
- package/cli/settings.js +6 -6
- package/cli/update.js +12 -11
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +65 -0
- package/extensions/ai-addons-updater/index.js +7 -3
- package/extensions/ai-addons-updater/index.ts +15 -3
- package/extensions/caveman-session/index.js +17 -12
- package/extensions/caveman-session/index.ts +17 -12
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +22 -2
- package/extensions/combo-toggle/index.ts +26 -2
- package/extensions/lib/utils.js +20 -4
- package/extensions/lib/utils.ts +18 -4
- package/extensions/rtk-session/index.js +11 -10
- package/extensions/rtk-session/index.ts +10 -11
- package/extensions/shared/plugin-settings.js +37 -3
- package/extensions/shared/plugin-settings.ts +34 -3
- package/extensions/shared/rtk-gain.js +2 -2
- package/extensions/shared/rtk-gain.ts +3 -3
- package/extensions/shared/session-state.js +5 -3
- package/extensions/shared/session-state.ts +4 -3
- package/extensions/shared/types.ts +5 -0
- package/extensions/shared/usage-ledger.js +114 -4
- package/extensions/shared/usage-ledger.ts +122 -4
- package/extensions/tersio-commands/index.js +10 -13
- package/extensions/tersio-commands/index.ts +10 -13
- package/package.json +21 -12
- package/tersio.js +10 -10
- package/dashboard/charts.js +0 -1208
- package/dashboard/core.js +0 -357
- package/dashboard/settings.js +0 -166
- package/dashboard/share.js +0 -218
- package/dashboard/styles.css +0 -379
- package/dashboard/template.html +0 -515
- package/extensions/shared/mode-reinforcement.ts +0 -29
- package/gain/dist/brand.webp +0 -0
- package/gain/dist/index.html +0 -65
- package/gain/dist/vite.svg +0 -1
- /package/dashboard/{brand.webp → dist/brand.webp} +0 -0
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ Then restart OMP and enable a preset:
|
|
|
38
38
|
|
|
39
39
|
Individual toggles: `/caveman full` · `/rtk on` · `/ponytail full`. Everything starts off until you enable it.
|
|
40
40
|
|
|
41
|
+
When Tersio is installed through OMP, the first interactive launch asks for a session-start Combo preset once (`off`, `medium`, `balanced`, or `max`) and saves it as `comboDefault`. Running `tersio install` performs the same setup through the CLI.
|
|
42
|
+
|
|
41
43
|
Session-start defaults (prompted during `install`, or flags):
|
|
42
44
|
|
|
43
45
|
```bash
|
|
@@ -61,39 +63,45 @@ Windows/WSL have separate OMP homes — install from the environment where OMP r
|
|
|
61
63
|
|
|
62
64
|
## 📊 Benchmarks
|
|
63
65
|
|
|
64
|
-
Measured
|
|
66
|
+
Measured surfaces use different boundaries. Do not treat reply-text, code-diff, and shell-output reductions as total bill savings. Full protocol and caveats are in [BENCHMARK.md](./BENCHMARK.md).
|
|
65
67
|
|
|
66
68
|
| Measured surface (n) | Baseline → Tersio | Δ |
|
|
67
69
|
|---|---|---|
|
|
68
|
-
| Terse reply, caveman lite — BPE
|
|
69
|
-
| Terse reply, caveman full — BPE
|
|
70
|
-
| Terse reply, caveman ultra — BPE
|
|
71
|
-
| Code diff, ponytail lite — BPE
|
|
72
|
-
| Code diff, ponytail full — BPE
|
|
73
|
-
| Code diff, ponytail ultra — BPE
|
|
74
|
-
| Shell output, rtk
|
|
75
|
-
| Shell output, rtk
|
|
76
|
-
| Shell output, rtk
|
|
77
|
-
| Shell output, rtk
|
|
78
|
-
|
|
70
|
+
| Terse reply, caveman lite — BPE tokens (3 samples, p50) | 173 → 69 | **−60.1% reply text** |
|
|
71
|
+
| Terse reply, caveman full — BPE tokens (3 samples, p50) | 173 → 49 | **−71.7% reply text** |
|
|
72
|
+
| Terse reply, caveman ultra — BPE tokens (3 samples, p50) | 173 → 25 | **−85.5% reply text** |
|
|
73
|
+
| Code diff, ponytail lite — BPE tokens (2 tasks) | 326–462 → 57–121 | **−73.8…−82.5% diff** |
|
|
74
|
+
| Code diff, ponytail full — BPE tokens (2 tasks) | 326–462 → 55–88 | **−81.0…−83.1% diff** |
|
|
75
|
+
| Code diff, ponytail ultra — BPE tokens (2 tasks) | 326–462 → 40–88 | **−81.0…−87.7% diff** |
|
|
76
|
+
| Shell output, rtk `git status` | 182 → 72 | **−60.4% command output** |
|
|
77
|
+
| Shell output, rtk repo `grep` | 376 → 274 | **−27.1% command output** |
|
|
78
|
+
| Shell output, rtk `find` | 205 → 168 | **−18.0% command output** |
|
|
79
|
+
| Shell output, rtk `git diff` | 12,684 → 8,504 | **−33.0% command output** |
|
|
80
|
+
|
|
81
|
+
The Command tools table reports weighted RTK command-output savings and OMP-specific adoption from executed Bash records. It does not estimate provider billing.
|
|
82
|
+
Upstream Ponytail's fair agentic benchmark reports 54% less code, 22% fewer tokens, 20% lower cost, and 100% retained safety. RTK estimates command-output tokens, not bill savings. Caveman and Ponytail compliance varies by model. Measure your own sessions with `tersio usage` and the dashboard.
|
|
79
83
|
|
|
80
|
-
Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn costs 941 tok baseline vs 399 with Tersio (0.42×, **−57.6% per turn**). Overhead repays in turn 1 with the bundled floor (464 tok); turn 4 with the real installed Ponytail plugin (v4.9.0, 1,264 tok). RTK concentrates savings where noise lives — hook-wired sessions record **−97.5% on summarized test suites** (rtk `history.db`, 4 runs).
|
|
81
84
|
|
|
82
|
-
## 📈
|
|
85
|
+
## 📈 Dashboard
|
|
83
86
|
|
|
84
|
-
`tersio
|
|
87
|
+
`tersio dashboard --open` serves a local dashboard (127.0.0.1 only) that charts your own savings from `~/.tersio/usage.db`. Three views:
|
|
85
88
|
|
|
86
89
|
**Live feed and savings.** Token throughput, cost, savings bento, and recent activity — the top of the dashboard.
|
|
87
90
|
|
|
88
|
-

|
|
89
92
|
|
|
90
93
|
**Top models and recent requests.** Per-model cost breakdown plus the last requests with elapsed time and token speed.
|
|
91
94
|
|
|
92
|
-

|
|
93
96
|
|
|
94
97
|
**Command tools.** Token savings, average rate, and timing per shell tool.
|
|
95
98
|
|
|
96
|
-

|
|
100
|
+
|
|
101
|
+
### Measurement references
|
|
102
|
+
|
|
103
|
+
- **[EcoLogits](https://github.com/mlco2/ecologits)** informs Tersio's local CO₂ and energy estimates. Tersio ports the model to TypeScript; EcoLogits is not a runtime dependency.
|
|
104
|
+
- **[Tokscale](https://github.com/junhoyeo/tokscale)** informs Tersio's Input, Output, Cache Read, and Cache Write accounting from local OMP sessions. Tokscale is a reference, not a runtime dependency.
|
|
97
105
|
|
|
98
106
|
## 🖥️ CLI
|
|
99
107
|
|
|
@@ -104,12 +112,12 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn c
|
|
|
104
112
|
| `tersio reinstall` | Fresh install, preserving the Ponytail package |
|
|
105
113
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health (`--fix` repairs, `--fix=<scope>` one scope, `--dry-run` previews) |
|
|
106
114
|
| `tersio usage` | Ledger-backed usage + savings report |
|
|
107
|
-
| `tersio
|
|
115
|
+
| `tersio dashboard` | Open the Dashboard (`--open`, `--export <file>`, `--port <n>`, `--currency <code>`; serves localhost only) |
|
|
108
116
|
| `tersio reset` | Clear tersio statistics: usage ledger + a watermark that hides pre-reset rows from every derived view (Y/N confirm, `--dry-run`, `--yes`) — session transcripts and RTK history stay intact on disk |
|
|
109
117
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary and its `rtk.ts` OMP wiring) |
|
|
110
118
|
| `tersio version` | Print version |
|
|
111
119
|
|
|
112
|
-
Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`, `--currency <code>` (usage
|
|
120
|
+
Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`, `--currency <code>` (usage/dashboard display currency; flag wins, then the `tersio settings` default, then USD). Legacy `--doctor` / `--uninstall` forms still work.
|
|
113
121
|
|
|
114
122
|
## ⌨️ Commands reference
|
|
115
123
|
|
|
@@ -118,13 +126,13 @@ Mode switches live on their own commands; bare `/tersio` prints status.
|
|
|
118
126
|
| Command | Purpose |
|
|
119
127
|
|---|---|
|
|
120
128
|
| `/combo off\|medium\|balanced\|max\|status` | One preset for all three (start here): medium = lite/lite/on, balanced = full/full/on, max = ultra/ultra/on; per-mode tweaks drop to `custom`. |
|
|
121
|
-
| `/caveman lite\|full\|ultra\|wenyan\|off\|status` | Terse replies
|
|
122
|
-
| `/rtk on\|off\|status` |
|
|
123
|
-
| `/ponytail off\|lite\|full\|ultra\|
|
|
129
|
+
| `/caveman lite\|full\|ultra\|wenyan-lite\|wenyan-full\|wenyan-ultra\|off\|status` | Terse replies with upstream-aligned Caveman levels. Legacy `wenyan` restores as `wenyan-full`. |
|
|
130
|
+
| `/rtk on\|off\|status` | Controls RTK session guidance, `rtk_run`, and the automatic hook through shared `RTK_DISABLED` state. Exact bytes bypass RTK. |
|
|
131
|
+
| `/ponytail off\|lite\|full\|ultra\|status` | Minimal code using upstream Ponytail modes. Review runs separately through `/ponytail-review`. |
|
|
124
132
|
| `/tersio status` | Active modes + combo level; state persists, propagates to subagents, and lights the Combo footer indicator. |
|
|
125
133
|
| `/tersio check` | Add-on version check |
|
|
126
134
|
| `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | Update add-ons, preview with dry-run |
|
|
127
|
-
| `/tersio
|
|
135
|
+
| `/tersio dashboard` | Open the Dashboard. |
|
|
128
136
|
| `/tersio usage` | Ledger report for this machine (rows in `~/.tersio/usage.db`, local only) |
|
|
129
137
|
| `/tersio help` | This table |
|
|
130
138
|
| `/ai-addons` | Add-on updater alias, still works. |
|
|
@@ -133,11 +141,11 @@ Mode switches live on their own commands; bare `/tersio` prints status.
|
|
|
133
141
|
|
|
134
142
|
| What | Path |
|
|
135
143
|
|---|---|
|
|
136
|
-
| Caveman / RTK /
|
|
144
|
+
| Caveman / RTK / Combo / Tersio commands | `~/.omp/plugins/node_modules/@krtclcdy/tersio/extensions/{caveman-session,rtk-session,combo-toggle,tersio-commands}/` — loaded from Tersio's OMP plugin manifest |
|
|
137
145
|
| RTK OMP wiring (rtk-owned) | `~/.omp/agent/extensions/rtk.ts` — written by the installer via `rtk init -g --agent omp`; auto-loads, no config entry |
|
|
138
|
-
| Ponytail package (bundled
|
|
139
|
-
| RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows) |
|
|
140
|
-
|
|
|
146
|
+
| Ponytail package (bundled Tersio dependency — one Plugins row, updates with `tersio update`) | `~/.omp/plugins/node_modules/@dietrichgebert/ponytail/` — loaded as a nested plugin dependency |
|
|
147
|
+
| RTK binary | Installer writes `~/.bun/bin/rtk` (`rtk.exe` on Windows). Runtime resolves `PATH` first, then this managed path. |
|
|
148
|
+
| Legacy config cleanup | `~/.omp/agent/config.yml` — doctor removes retired or duplicate extension entries |
|
|
141
149
|
|
|
142
150
|
The installer writes `<file>.bak` before replacing an extension source; the updater keeps `rtk.bak` / `rule.md.bak` and restores them if the replacement fails validation.
|
|
143
151
|
|
|
@@ -145,11 +153,11 @@ User-level installs also register the package in `~/.omp/plugins` (visible in OM
|
|
|
145
153
|
|
|
146
154
|
## 🛠️ Troubleshooting
|
|
147
155
|
|
|
148
|
-
**Ponytail or Combo command missing:**
|
|
156
|
+
**Ponytail or Combo command missing:** confirm `~/.omp/plugins/package.json` lists `@krtclcdy/tersio`, then run `tersio reinstall`, restart OMP, and check `tersio doctor`.
|
|
149
157
|
|
|
150
158
|
**RTK missing or not executable:** run `tersio reinstall`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
|
|
151
159
|
|
|
152
|
-
**RTK commands not metered in the
|
|
160
|
+
**RTK commands not metered in the Dashboard:** check `tersio doctor` — the `RTK OMP wiring (rtk.ts)` row must be ok. Missing: run `rtk init -g --agent omp` (needs rtk ≥ 0.49) and restart OMP. Native tool calls (`read`/`edit`/`eval`) stay unmetered — only bash tool calls pass through rtk.
|
|
153
161
|
|
|
154
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.
|
|
155
163
|
|
package/cli/common.js
CHANGED
|
@@ -16,9 +16,8 @@ const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
|
|
|
16
16
|
const PACKAGE_NAME = '@krtclcdy/tersio';
|
|
17
17
|
const PACKAGE_BIN = 'tersio';
|
|
18
18
|
const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('../package.json');
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'review']);
|
|
19
|
+
const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
20
|
+
const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra']);
|
|
22
21
|
const RTK_DEFAULTS = new Set(['on', 'off']);
|
|
23
22
|
const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
24
23
|
// ponytail: Combo preset implies all three modes. Mirrors COMBO_LEVELS in
|
|
@@ -48,7 +47,7 @@ function flagValue(name) {
|
|
|
48
47
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
49
48
|
}
|
|
50
49
|
// --- CLI flags ---
|
|
51
|
-
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true,
|
|
50
|
+
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, dashboard: true, reset: true, settings: true, version: true, help: true };
|
|
52
51
|
const args = process.argv.slice(2);
|
|
53
52
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
54
53
|
const command = commandArg?.toLowerCase() || null;
|
|
@@ -75,13 +74,13 @@ const verbose = args.includes('--verbose');
|
|
|
75
74
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
76
75
|
const fix = args.includes('--fix') || args.some((a) => a.startsWith('--fix='));
|
|
77
76
|
const usage = command === 'usage' || args.includes('--usage');
|
|
78
|
-
const
|
|
77
|
+
const dashboard = command === 'dashboard';
|
|
79
78
|
const reset = command === 'reset';
|
|
80
79
|
const settings = command === 'settings';
|
|
81
80
|
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
82
81
|
const dashboardOpen = args.includes('--open');
|
|
83
82
|
const dashboardExport = flagValue('--export') ?? null;
|
|
84
|
-
// Display currency for usage/
|
|
83
|
+
// Display currency for usage/dashboard: --currency flag wins, then the stored
|
|
85
84
|
// plugin default (tersio settings), then USD.
|
|
86
85
|
const currency = parseCurrencyFlag(flagValue('--currency')) ?? readStoredCurrency();
|
|
87
86
|
const currencyGiven = flagValue('--currency') !== undefined;
|
|
@@ -167,7 +166,26 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
167
166
|
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
168
167
|
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
169
168
|
}
|
|
170
|
-
|
|
169
|
+
const matchingEntries = lines.filter((entry) => entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '') === normalizedPath);
|
|
170
|
+
if (matchingEntries.length > 1) {
|
|
171
|
+
if (options.dryRun) {
|
|
172
|
+
if (verbose && !options.quiet)
|
|
173
|
+
console.log(` [dry-run] would remove ${matchingEntries.length - 1} duplicate ${label} registration${matchingEntries.length === 2 ? '' : 's'}`);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
let kept = false;
|
|
177
|
+
lines = lines.filter((entry) => {
|
|
178
|
+
if (entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '') !== normalizedPath)
|
|
179
|
+
return true;
|
|
180
|
+
if (kept)
|
|
181
|
+
return false;
|
|
182
|
+
kept = true;
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
185
|
+
await writeConfigLines(configPath, lines, ` [write] Removed duplicate ${label} registration`, options);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (matchingEntries.length === 1) {
|
|
171
189
|
debug(`${label} already in config.yml`);
|
|
172
190
|
return false;
|
|
173
191
|
}
|
|
@@ -191,43 +209,29 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
191
209
|
await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`, options);
|
|
192
210
|
return true;
|
|
193
211
|
}
|
|
194
|
-
async function
|
|
195
|
-
const normalizedPath = extensionPath.replace(/\\/g, '/');
|
|
196
|
-
const
|
|
197
|
-
|
|
212
|
+
async function removeExtensionFromConfig(configPath, extensionPath, label, options = {}) {
|
|
213
|
+
const normalizedPath = extensionPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
214
|
+
const extensionSuffix = normalizedPath.includes('/extensions/')
|
|
215
|
+
? normalizedPath.slice(normalizedPath.indexOf('/extensions/') + 1)
|
|
216
|
+
: normalizedPath;
|
|
198
217
|
const raw = await readTextIfExists(configPath);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
218
|
+
if (raw === null)
|
|
219
|
+
return false;
|
|
220
|
+
const lines = raw.split('\n');
|
|
221
|
+
const matches = (entry) => {
|
|
222
|
+
const value = entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
223
|
+
return value === extensionSuffix || value.endsWith(`/${extensionSuffix}`);
|
|
224
|
+
};
|
|
225
|
+
const kept = lines.filter((entry) => !matches(entry));
|
|
226
|
+
const removed = lines.length - kept.length;
|
|
227
|
+
if (removed === 0)
|
|
208
228
|
return false;
|
|
209
229
|
if (options.dryRun) {
|
|
210
230
|
if (verbose && !options.quiet)
|
|
211
|
-
console.log(` [dry-run] would
|
|
231
|
+
console.log(` [dry-run] would remove ${label} registration${removed === 1 ? '' : 's'}`);
|
|
212
232
|
return true;
|
|
213
233
|
}
|
|
214
|
-
|
|
215
|
-
lines.splice(existingIndex, 1);
|
|
216
|
-
const refreshedAfterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
|
|
217
|
-
if (refreshedAfterIndex !== -1) {
|
|
218
|
-
lines.splice(refreshedAfterIndex + 1, 0, line);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
const extensionsIndex = lines.findIndex((entry) => EXTENSIONS_KEY_RE.test(entry));
|
|
222
|
-
if (extensionsIndex === -1) {
|
|
223
|
-
lines.push('extensions:', line, '');
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
normalizeExtensionsKey(lines);
|
|
227
|
-
lines.splice(extensionsIndex + 1, 0, line);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`, options);
|
|
234
|
+
await writeConfigLines(configPath, kept, ` [write] Removed duplicate ${label} registration`, options);
|
|
231
235
|
return true;
|
|
232
236
|
}
|
|
233
237
|
function readPonytailConfig() {
|
|
@@ -298,4 +302,4 @@ function relTime(ageMs) {
|
|
|
298
302
|
const weeks = Math.floor(days / 7);
|
|
299
303
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
300
304
|
}
|
|
301
|
-
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, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage,
|
|
305
|
+
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, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
|
package/cli/currency.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/currency.ts — display-currency helpers (USD-base, offline snapshot rates).
|
|
2
|
-
// Same 10 currencies, symbols, and snapshot rates as the
|
|
3
|
-
// converter (dashboard/app
|
|
2
|
+
// Same 10 currencies, symbols, and snapshot rates as the dashboard's
|
|
3
|
+
// converter (dashboard/app); the dashboard replaces rates with live
|
|
4
4
|
// frankfurter figures when reachable, the CLI always uses the snapshot.
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import os from 'node:os';
|
package/cli/dashboard.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// cli/dashboard.ts —
|
|
2
|
-
// template (no deps, no build) plus a /data.json endpoint from the ledger.
|
|
1
|
+
// cli/dashboard.ts — serves the built shadcn Dashboard with local usage APIs.
|
|
3
2
|
// Binds 127.0.0.1 only; --export writes a file://-ready file instead.
|
|
4
3
|
// Note: no Promise.withResolvers here — engines still allow Node 20.12,
|
|
5
4
|
// which lacks it; the listening server itself keeps the process alive.
|
|
@@ -14,24 +13,17 @@ import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usa
|
|
|
14
13
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
15
14
|
import { summarizeUsage } from './usage.js';
|
|
16
15
|
import { isCurrencyCode } from './currency.js';
|
|
17
|
-
import {
|
|
16
|
+
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
18
17
|
import { storedProfile, writePluginSettings } from './profile.js';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// shadcn gain app (gain/). When its production bundle exists the server
|
|
29
|
-
// binds to it; otherwise it falls back to the legacy dashboard/ segments.
|
|
30
|
-
const GAIN_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'gain', 'dist');
|
|
31
|
-
const GAIN_INDEX = path.join(GAIN_DIST, 'index.html');
|
|
32
|
-
const GAIN_BRAND = path.join(GAIN_DIST, 'brand.webp');
|
|
33
|
-
function useGainBundle() {
|
|
34
|
-
return existsSync(GAIN_INDEX);
|
|
18
|
+
import { PACKAGE_NAME } from './common.js';
|
|
19
|
+
import { resolveRtkBinary } from '../extensions/lib/utils.js';
|
|
20
|
+
const DASHBOARD_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard', 'dist');
|
|
21
|
+
const DASHBOARD_INDEX = path.join(DASHBOARD_DIST, 'index.html');
|
|
22
|
+
const DASHBOARD_BRAND = path.join(DASHBOARD_DIST, 'brand.webp');
|
|
23
|
+
function requireDashboardBundle() {
|
|
24
|
+
if (existsSync(DASHBOARD_INDEX))
|
|
25
|
+
return;
|
|
26
|
+
throw new Error('Dashboard bundle missing. Run `bun run build` before using `tersio dashboard`.');
|
|
35
27
|
}
|
|
36
28
|
function contentType(file) {
|
|
37
29
|
if (file.endsWith('.html'))
|
|
@@ -53,9 +45,9 @@ function contentType(file) {
|
|
|
53
45
|
async function readSegment(file) {
|
|
54
46
|
return fs.readFile(file, 'utf8');
|
|
55
47
|
}
|
|
56
|
-
async function
|
|
57
|
-
const
|
|
58
|
-
return `data:image/webp;base64,${
|
|
48
|
+
async function brandDataUri() {
|
|
49
|
+
const brand = await fs.readFile(DASHBOARD_BRAND);
|
|
50
|
+
return `data:image/webp;base64,${brand.toString('base64')}`;
|
|
59
51
|
}
|
|
60
52
|
function dataJson() {
|
|
61
53
|
return JSON.stringify(summarizeUsage(readUsage()));
|
|
@@ -109,8 +101,8 @@ function ompDefaultModel() {
|
|
|
109
101
|
}
|
|
110
102
|
}
|
|
111
103
|
function healthJson() {
|
|
112
|
-
const rtkBin =
|
|
113
|
-
const rtkPresent =
|
|
104
|
+
const rtkBin = resolveRtkBinary();
|
|
105
|
+
const rtkPresent = rtkBin !== null;
|
|
114
106
|
const detectedOmpPath = ompPath();
|
|
115
107
|
return JSON.stringify({
|
|
116
108
|
tersio: PACKAGE_VERSION,
|
|
@@ -119,7 +111,7 @@ function healthJson() {
|
|
|
119
111
|
omp: ompVersion(detectedOmpPath),
|
|
120
112
|
ompPath: detectedOmpPath,
|
|
121
113
|
provider: ompDefaultModel(),
|
|
122
|
-
rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin },
|
|
114
|
+
rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin || 'not found in PATH' },
|
|
123
115
|
home: tersioHomePath(),
|
|
124
116
|
});
|
|
125
117
|
}
|
|
@@ -194,26 +186,39 @@ function relAge(ms) {
|
|
|
194
186
|
function computeDoctorRows() {
|
|
195
187
|
const rows = [];
|
|
196
188
|
const extDir = path.join(OMP_AGENT_DIR, 'extensions');
|
|
197
|
-
const rtkBin =
|
|
189
|
+
const rtkBin = resolveRtkBinary();
|
|
198
190
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
191
|
+
const tersioPluginDir = path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'));
|
|
199
192
|
const ok = (p) => existsSync(p);
|
|
193
|
+
const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
|
|
200
194
|
const ext = (label, p) => {
|
|
201
195
|
rows.push({ label, ok: ok(p), detail: ok(p) ? 'installed' : p, group: 'Extensions' });
|
|
202
196
|
};
|
|
203
197
|
const addon = (label, present, detail, group = 'Add-ons') => {
|
|
204
198
|
rows.push({ label, ok: present, detail, group });
|
|
205
199
|
};
|
|
206
|
-
|
|
207
|
-
|
|
200
|
+
let explicitEntries = [];
|
|
201
|
+
try {
|
|
202
|
+
explicitEntries = readFileSync(configPath, 'utf8').split('\n')
|
|
203
|
+
.map((line) => line.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, ''))
|
|
204
|
+
.filter((line) => line.startsWith('/') || line.startsWith('.'));
|
|
205
|
+
}
|
|
206
|
+
catch { /* missing config is reported by extension rows */ }
|
|
207
|
+
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
208
|
+
addon('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : 'ok', 'Extensions');
|
|
209
|
+
const retiredReinforcement = explicitEntries.filter((entry) => entry.endsWith('/shared/mode-reinforcement.ts')).length;
|
|
210
|
+
addon('No retired reinforcement', retiredReinforcement === 0, retiredReinforcement ? `${retiredReinforcement} registration(s)` : 'ok', 'Extensions');
|
|
211
|
+
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
|
|
212
|
+
ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
|
|
208
213
|
ext('Ponytail extension', path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
|
|
209
|
-
const rule = path.join(
|
|
214
|
+
const rule = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'rule.md');
|
|
210
215
|
const ruleAge = ageStr(rule);
|
|
211
216
|
const ruleAt = absTime(rule);
|
|
212
217
|
addon('Caveman rule', ruleAge !== null, ruleAge && ruleAt ? `(updated ${ruleAge} · ${ruleAt})` : rule);
|
|
213
|
-
const rtkVer =
|
|
214
|
-
const rtkAge = ageStr(rtkBin);
|
|
215
|
-
const rtkAt = absTime(rtkBin);
|
|
216
|
-
addon('RTK binary', rtkVer !== null, rtkVer && rtkAge && rtkAt ? `${rtkVer} (updated ${rtkAge} · ${rtkAt})` : rtkBin);
|
|
218
|
+
const rtkVer = rtkBin ? rtkVersion(rtkBin) : null;
|
|
219
|
+
const rtkAge = rtkBin ? ageStr(rtkBin) : null;
|
|
220
|
+
const rtkAt = rtkBin ? absTime(rtkBin) : null;
|
|
221
|
+
addon('RTK binary', rtkVer !== null, rtkVer && rtkAge && rtkAt ? `${rtkVer} (updated ${rtkAge} · ${rtkAt})` : rtkBin || 'not found in PATH');
|
|
217
222
|
const wiring = path.join(extDir, 'rtk.ts');
|
|
218
223
|
addon('RTK OMP wiring (rtk.ts)', ok(wiring), ok(wiring) ? 'loaded' : wiring);
|
|
219
224
|
let ponytailVer = null;
|
|
@@ -273,7 +278,7 @@ function readDiagSchedule() {
|
|
|
273
278
|
return readDiagReport()?.schedule ?? 'manual';
|
|
274
279
|
}
|
|
275
280
|
// Persist the dashboard's picker choice so close → reopen keeps it: each
|
|
276
|
-
// `tersio
|
|
281
|
+
// `tersio dashboard` run serves a fresh ephemeral port (a new origin), so the
|
|
277
282
|
// browser's localStorage alone cannot survive a restart. The stored default
|
|
278
283
|
// feeds data.json and `tersio usage` on the next run.
|
|
279
284
|
async function saveDashboardCurrency(raw) {
|
|
@@ -310,45 +315,22 @@ function escapeInline(json) {
|
|
|
310
315
|
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
311
316
|
}
|
|
312
317
|
async function exportDashboard(exportFile) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
console.log(`[ok] gain exported → ${exportFile}`);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const [template, css, core, charts, settings, share, legacyIcon] = await Promise.all([
|
|
324
|
-
readSegment(TEMPLATE), readSegment(STYLES), readSegment(CORE_JS), readSegment(CHARTS_JS),
|
|
325
|
-
readSegment(SETTINGS_JS), readSegment(SHARE_JS), faviconDataUri(),
|
|
326
|
-
]);
|
|
327
|
-
const inline = template
|
|
328
|
-
.replace('<link rel="stylesheet" href="styles.css">', () => `<style>\n${css}</style>`)
|
|
329
|
-
.replace('<script src="core.js" defer></script>', () => `<script>\n${core}</script>`)
|
|
330
|
-
.replace('<script src="charts.js" defer></script>', () => `<script>\n${charts}</script>`)
|
|
331
|
-
.replace('<script src="settings.js" defer></script>', () => `<script>\n${settings}</script>`)
|
|
332
|
-
.replace('<script src="share.js" defer></script>', () => `<script>\n${share}</script>`)
|
|
333
|
-
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({ health: JSON.parse(healthJson()), doctor: getDoctorReport(false) }))};`)
|
|
334
|
-
.replace("fetch('data.json')", () => `Promise.resolve({ json: function () { return ${escapeInline(dataJson())}; } })`)
|
|
335
|
-
// Exported file runs on file:// where load() early-returns before the
|
|
336
|
-
// stub above ever runs, so nothing ever renders. Drop that guard in
|
|
337
|
-
// the export only; template.html keeps it to avoid 404 polling loops.
|
|
338
|
-
.replace("if (window.location.protocol === 'file:') return;", () => `if (false) return;`)
|
|
339
|
-
.replace('href="brand.webp"', () => `href="${legacyIcon}"`)
|
|
340
|
-
.replace(/src="brand.webp"/g, () => `src="${legacyIcon}"`);
|
|
318
|
+
requireDashboardBundle();
|
|
319
|
+
const [bundle, icon] = await Promise.all([readSegment(DASHBOARD_INDEX), brandDataUri()]);
|
|
320
|
+
const inline = bundle
|
|
321
|
+
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({ data: JSON.parse(dataJson()), health: JSON.parse(healthJson()), doctor: getDoctorReport(false) }))};`)
|
|
322
|
+
.replace(/href="brand\.webp"/g, () => `href="${icon}"`)
|
|
323
|
+
.replace(/src="brand\.webp"/g, () => `src="${icon}"`)
|
|
324
|
+
.replace('fetch("brand.webp")', () => `Promise.resolve({ ok: true, blob: async () => new Blob([atob("${icon.split(',')[1]}")], { type: "image/webp" }) })`);
|
|
341
325
|
await fs.writeFile(exportFile, inline, 'utf8');
|
|
342
|
-
console.log(`[ok]
|
|
326
|
+
console.log(`[ok] Dashboard exported → ${exportFile}`);
|
|
343
327
|
}
|
|
344
328
|
async function runDashboard(options) {
|
|
345
329
|
if (options.exportFile) {
|
|
346
330
|
await exportDashboard(options.exportFile);
|
|
347
331
|
return;
|
|
348
332
|
}
|
|
349
|
-
|
|
350
|
-
? null
|
|
351
|
-
: await withInteractiveSpinner('Loading dashboard template', () => readSegment(TEMPLATE));
|
|
333
|
+
requireDashboardBundle();
|
|
352
334
|
const server = http.createServer(async (req, res) => {
|
|
353
335
|
if (req.url === '/reset' && req.method === 'POST') {
|
|
354
336
|
const rows = clearUsageLedger();
|
|
@@ -419,67 +401,29 @@ async function runDashboard(options) {
|
|
|
419
401
|
res.end(JSON.stringify(getDoctorReport(fresh)));
|
|
420
402
|
return;
|
|
421
403
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
'/settings.js': SETTINGS_JS,
|
|
431
|
-
'/share.js': SHARE_JS,
|
|
432
|
-
};
|
|
433
|
-
if (req.url !== undefined && req.url in scripts) {
|
|
434
|
-
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8' });
|
|
435
|
-
res.end(await readSegment(scripts[req.url]));
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
if (req.url === '/brand.webp') {
|
|
439
|
-
// Prefer the bundled brand; fall back to the legacy dashboard copy.
|
|
440
|
-
for (const file of [GAIN_BRAND, BRAND]) {
|
|
441
|
-
try {
|
|
442
|
-
const png = await fs.readFile(file);
|
|
443
|
-
res.writeHead(200, { 'Content-Type': 'image/webp' });
|
|
444
|
-
res.end(png);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
catch { /* try the next copy */ }
|
|
448
|
-
}
|
|
449
|
-
res.writeHead(404);
|
|
450
|
-
res.end();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (useGainBundle()) {
|
|
454
|
-
// shadcn bundle: index.html at /, sibling files (brand.webp,
|
|
455
|
-
// vite.svg) by path. No path escapes outside gain/dist.
|
|
456
|
-
const name = (req.url ?? '/').split('?')[0];
|
|
457
|
-
const file = path.normalize(path.join(GAIN_DIST, name === '/' ? 'index.html' : name.slice(1)));
|
|
458
|
-
if (file === GAIN_DIST || file.startsWith(GAIN_DIST + path.sep)) {
|
|
459
|
-
try {
|
|
460
|
-
const body = await fs.readFile(file);
|
|
461
|
-
res.writeHead(200, { 'Content-Type': contentType(file) });
|
|
462
|
-
res.end(body);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
catch { /* fall through to index */ }
|
|
404
|
+
const name = (req.url ?? '/').split('?')[0];
|
|
405
|
+
const file = path.normalize(path.join(DASHBOARD_DIST, name === '/' ? 'index.html' : name.slice(1)));
|
|
406
|
+
if (file === DASHBOARD_DIST || file.startsWith(DASHBOARD_DIST + path.sep)) {
|
|
407
|
+
try {
|
|
408
|
+
const body = await fs.readFile(file);
|
|
409
|
+
res.writeHead(200, { 'Content-Type': contentType(file) });
|
|
410
|
+
res.end(body);
|
|
411
|
+
return;
|
|
466
412
|
}
|
|
467
|
-
|
|
468
|
-
res.end(await readSegment(GAIN_INDEX));
|
|
469
|
-
return;
|
|
413
|
+
catch { /* fall through to index */ }
|
|
470
414
|
}
|
|
471
415
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
472
|
-
res.end(
|
|
416
|
+
res.end(await readSegment(DASHBOARD_INDEX));
|
|
473
417
|
});
|
|
474
418
|
server.listen(options.port, '127.0.0.1', () => {
|
|
475
419
|
try {
|
|
476
|
-
process.title = 'tersio
|
|
420
|
+
process.title = 'tersio dashboard';
|
|
477
421
|
}
|
|
478
422
|
catch { /* non-POSIX shells keep node */ }
|
|
479
423
|
const address = server.address();
|
|
480
424
|
const port = typeof address === 'object' && address ? address.port : options.port;
|
|
481
425
|
const url = `http://127.0.0.1:${port}`;
|
|
482
|
-
console.log(`[ok]
|
|
426
|
+
console.log(`[ok] Dashboard live → ${url} (Ctrl-C to stop)`);
|
|
483
427
|
if (options.open)
|
|
484
428
|
openBrowser(url);
|
|
485
429
|
});
|