@krtclcdy/tersio 2.9.0 → 2.11.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 +54 -36
- package/cli/banner.js +84 -0
- package/cli/common.js +3 -2
- package/cli/dashboard.js +37 -11
- package/cli/doctor.js +8 -1
- package/cli/install.js +27 -7
- package/cli/reset.js +51 -0
- package/cli/update.js +8 -0
- package/cli/usage.js +87 -22
- package/dashboard/app.js +922 -0
- package/dashboard/styles.css +194 -0
- package/dashboard/template.html +305 -0
- package/extensions/caveman-session/index.js +12 -1
- package/extensions/combo-toggle/index.js +3 -3
- package/extensions/rtk-session/index.js +8 -1
- package/extensions/shared/carbon.js +88 -0
- package/extensions/shared/pricing.js +121 -0
- package/extensions/shared/rtk-gain.js +61 -0
- package/extensions/shared/session-state.js +10 -3
- package/extensions/shared/usage-ledger.js +81 -37
- package/extensions/tersio-commands/index.js +5 -10
- package/package.json +2 -3
- package/tersio.js +8 -1
- package/cli/dashboard.html +0 -780
- /package/{cli → dashboard}/brand.webp +0 -0
package/README.md
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://github.com/KurutoDenzeru/tersio/releases)
|
|
5
|
-
[](./LICENSE)
|
|
6
|
-
[](https://nodejs.org)
|
|
7
|
-
[](https://github.com/KurutoDenzeru/tersio/actions)
|
|
8
|
-
[](https://github.com/KurutoDenzeru/tersio/issues)
|
|
3
|
+
# ✂️ Tersio — Token-saving OMP Add-ons
|
|
9
4
|
|
|
10
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@krtclcdy/tersio)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://www.npmjs.com/package/@krtclcdy/tersio)
|
|
8
|
+
[](https://github.com/KurutoDenzeru/tersio/actions)
|
|
9
|
+
[](./LICENSE)
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
Terse replies, compact shell output, and minimal code decisions for [Oh My Pi (OMP)](https://github.com/can1357/oh-my-pi) — one-command combo presets.
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- **Caveman** — Shortens replies, keeps technical substance. Modes: `lite`, `full`, `ultra`, `wenyan`.
|
|
16
|
+
- **RTK** — Routes noisy shell commands through the RTK binary for compact output; exact bytes bypass it by policy.
|
|
17
|
+
- **Ponytail** — Minimal, YAGNI-oriented code decisions, from nudge (`lite`) to requirement-challenger (`ultra`).
|
|
18
|
+
- **Combo** — Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max`.
|
|
19
|
+
- **Updater** — `/tersio check` and `/tersio update` cover Ponytail, RTK, and Caveman in-session, with dry-run and backups.
|
|
20
|
+
- **Gain dashboard** — `tersio gain` serves a local-only savings dashboard: cost with currency conversion, activity graph, command tools, and model breakdowns.
|
|
21
|
+
- **Usage ledger** — `tersio usage` prints a ledger-backed savings report for the machine, local only.
|
|
22
|
+
|
|
23
|
+
## 🧱 Tech Stack
|
|
24
|
+
|
|
25
|
+
- [TypeScript](https://www.typescriptlang.org/) + [Node.js](https://nodejs.org/): CLI and OMP extensions, compiled to dependency-free JS for install.
|
|
26
|
+
- Vanilla dashboard: dependency-free HTML/CSS/JS export — no build step, no runtime deps.
|
|
27
|
+
- [js-tiktoken](https://github.com/dqbd/tiktoken): Real `o200k_base` BPE token counts for benchmarks (dev only).
|
|
28
|
+
- [tokscale](https://github.com/junhoyeo/tokscale): Token-bucket convention (input / output / cache read / cache write) for parsing OMP session transcripts.
|
|
29
|
+
- [EcoLogits](https://github.com/genai-impact/ecologits): CO2 model (v0.8.2 port — per-model params, provider grids, served ÷32 amortization).
|
|
30
|
+
|
|
31
|
+
## ⚡ Getting Started
|
|
13
32
|
|
|
14
33
|
**Default** (macOS/Linux/WSL) — one line. Installs the CLI via npm, then runs the main installer (scope + Combo preset menus) in the same pass:
|
|
15
34
|
|
|
@@ -46,19 +65,20 @@ tersio install --combo-default balanced --caveman-default lite --rtk-default on
|
|
|
46
65
|
|
|
47
66
|
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
67
|
|
|
49
|
-
|
|
68
|
+
One-off use without installing:
|
|
50
69
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| **RTK** | Routes noisy shell commands through the RTK binary for compact output |
|
|
55
|
-
| **Ponytail** | Minimal, YAGNI-oriented code decisions |
|
|
56
|
-
| **Combo** | Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max` |
|
|
57
|
-
| **Updater** | `/tersio check` and `/tersio update` cover Ponytail, RTK, and Caveman in-session, with dry-run and backups |
|
|
70
|
+
```bash
|
|
71
|
+
npm exec --yes --prefer-online --package=@krtclcdy/tersio@latest -- tersio install
|
|
72
|
+
```
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
### Requirements
|
|
75
|
+
|
|
76
|
+
- [OMP](https://github.com/can1357/oh-my-pi)
|
|
77
|
+
- Node.js 20.12+ with npm
|
|
78
|
+
|
|
79
|
+
Windows/WSL have separate OMP homes — install from the environment where OMP runs. Inside WSL, `command -v npm` must resolve to a Linux path, not `/mnt/c/`.
|
|
60
80
|
|
|
61
|
-
## Benchmarks
|
|
81
|
+
## 📊 Benchmarks
|
|
62
82
|
|
|
63
83
|
Measured savings against the same workload without the modes. Token counts are real `o200k_base` BPE tokens (js-tiktoken), not chars÷4 estimates. Sample protocol, per-sample data, and caveats in [BENCHMARK.md](./BENCHMARK.md).
|
|
64
84
|
|
|
@@ -73,7 +93,7 @@ Measured savings against the same workload without the modes. Token counts are r
|
|
|
73
93
|
|
|
74
94
|
Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead ÷ saving-per-turn⌉ = ⌈480 ÷ 306⌉ = 2 turns`, where a mixed turn (reply + `git status`-class command + code task) costs 944 tok baseline vs 638 tok with Tersio — a 0.68× ratio. Every turn after turn 2 nets ≈ −32%. RTK keeps diffs and failing-test output exact by design (−0.4% and −1.5% there), concentrating savings where noise lives.
|
|
75
95
|
|
|
76
|
-
## CLI
|
|
96
|
+
## 🖥️ CLI
|
|
77
97
|
|
|
78
98
|
| Command | Purpose |
|
|
79
99
|
|---|---|
|
|
@@ -83,12 +103,13 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead
|
|
|
83
103
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
|
|
84
104
|
| `tersio usage` | Ledger-backed usage + savings report |
|
|
85
105
|
| `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`; serves localhost only) |
|
|
106
|
+
| `tersio reset` | Clear tersio statistics (usage ledger; `--dry-run`, `--yes`) — sessions and RTK history stay intact |
|
|
86
107
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
|
|
87
108
|
| `tersio version` | Print version |
|
|
88
109
|
|
|
89
110
|
Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--scope`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`. Legacy `--doctor` / `--uninstall` forms still work.
|
|
90
111
|
|
|
91
|
-
## Commands reference
|
|
112
|
+
## ⌨️ Commands reference
|
|
92
113
|
|
|
93
114
|
Everything runs under one root. Bare `/tersio` prints status.
|
|
94
115
|
|
|
@@ -106,7 +127,7 @@ Everything runs under one root. Bare `/tersio` prints status.
|
|
|
106
127
|
| `/tersio help` | This table |
|
|
107
128
|
| `/caveman`, `/rtk`, `/combo`, `/ai-addons` | Legacy aliases, still work; prefer `/tersio`. |
|
|
108
129
|
|
|
109
|
-
## Files and backups
|
|
130
|
+
## 🗂️ Files and backups
|
|
110
131
|
|
|
111
132
|
| What | Path |
|
|
112
133
|
|---|---|
|
|
@@ -117,27 +138,24 @@ Everything runs under one root. Bare `/tersio` prints status.
|
|
|
117
138
|
|
|
118
139
|
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.
|
|
119
140
|
|
|
120
|
-
|
|
141
|
+
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.
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
- Node.js 20.12+ with npm
|
|
143
|
+
## 🛠️ Troubleshooting
|
|
124
144
|
|
|
125
|
-
|
|
145
|
+
**Ponytail or Combo command missing:** run `tersio reinstall` in OMP's environment, restart OMP, then `tersio doctor` (repairs `config.yml` registrations).
|
|
126
146
|
|
|
127
|
-
|
|
147
|
+
**RTK missing or not executable:** run `tersio reinstall`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
|
|
128
148
|
|
|
129
|
-
|
|
130
|
-
npm exec --yes --prefer-online --package=@krtclcdy/tersio@latest -- tersio install
|
|
131
|
-
```
|
|
149
|
+
**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.
|
|
132
150
|
|
|
133
|
-
##
|
|
151
|
+
## 🤝🏻 Contributing
|
|
134
152
|
|
|
135
|
-
|
|
153
|
+
Contributions are always welcome, whether you're fixing bugs, improving docs, or shipping new features that make the project better for everyone.
|
|
136
154
|
|
|
137
|
-
|
|
155
|
+
Check out [Contributing.md](Contributing.md) to learn how to get started and follow the recommended workflow.
|
|
138
156
|
|
|
139
|
-
|
|
157
|
+
## ⚖️ License
|
|
140
158
|
|
|
141
|
-
|
|
159
|
+
This project is released under the MIT License, giving you the freedom to use, modify, and distribute the code with minimal restrictions.
|
|
142
160
|
|
|
143
|
-
[
|
|
161
|
+
For the full legal text, see the [LICENSE](LICENSE) file.
|
package/cli/banner.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// cli/banner.ts — mini pixel-scissor welcome (16x12 px -> 6 half-block rows).
|
|
2
|
+
// OMP-style launch moment for interactive runs: clack intro + note box.
|
|
3
|
+
// TTY-only and skipped on --dry-run, so piped output stays byte-identical.
|
|
4
|
+
import { intro as clackIntro, note as clackNote } from '@clack/prompts';
|
|
5
|
+
import { PACKAGE_VERSION, dryRun } from './common.js';
|
|
6
|
+
function tty() {
|
|
7
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
8
|
+
}
|
|
9
|
+
// Pixel classes: B/b blades, P pivot, L/R handle rings, l shafts.
|
|
10
|
+
const GRID = [
|
|
11
|
+
'..BB........bb..',
|
|
12
|
+
'..BBB......bbb..',
|
|
13
|
+
'....BB....bb....',
|
|
14
|
+
'.....BBBbbb.....',
|
|
15
|
+
'......BPPb......',
|
|
16
|
+
'....L.lPPl..R...',
|
|
17
|
+
'..LLLLLl.lRRRRR.',
|
|
18
|
+
'..LL.LL...RR.RR.',
|
|
19
|
+
'.LL...LL.RR...RR',
|
|
20
|
+
'..LL.LL...RR.RR.',
|
|
21
|
+
'..LLLLL...RRRRR.',
|
|
22
|
+
'....L.......R...',
|
|
23
|
+
];
|
|
24
|
+
// [truecolor rgb, ansi256] per class. Left ring red, right ring yellow.
|
|
25
|
+
const PALETTE = {
|
|
26
|
+
B: ['190;200;210', '252'],
|
|
27
|
+
b: ['190;200;210', '252'],
|
|
28
|
+
P: ['212;175;55', '172'],
|
|
29
|
+
L: ['239;68;68', '196'],
|
|
30
|
+
R: ['250;204;21', '220'],
|
|
31
|
+
l: ['190;200;210', '252'],
|
|
32
|
+
};
|
|
33
|
+
function bannerTier() {
|
|
34
|
+
if (process.env.NO_COLOR !== undefined)
|
|
35
|
+
return 'plain';
|
|
36
|
+
if (/truecolor|24bit/i.test(process.env.COLORTERM ?? ''))
|
|
37
|
+
return 'true';
|
|
38
|
+
if (/256color/i.test(process.env.TERM ?? ''))
|
|
39
|
+
return '256';
|
|
40
|
+
return 'plain';
|
|
41
|
+
}
|
|
42
|
+
function paint(ka, kb, tier) {
|
|
43
|
+
if (!ka && !kb)
|
|
44
|
+
return ' ';
|
|
45
|
+
if (tier === 'plain') {
|
|
46
|
+
if (ka && kb)
|
|
47
|
+
return '█';
|
|
48
|
+
return ka ? '▀' : '▄';
|
|
49
|
+
}
|
|
50
|
+
const fg = (k) => (tier === 'true' ? `\x1b[38;2;${PALETTE[k][0]}m` : `\x1b[38;5;${PALETTE[k][1]}m`);
|
|
51
|
+
const bg = (k) => (tier === 'true' ? `\x1b[48;2;${PALETTE[k][0]}m` : `\x1b[48;5;${PALETTE[k][1]}m`);
|
|
52
|
+
if (ka && kb && ka === kb)
|
|
53
|
+
return `${fg(ka)}█\x1b[0m`;
|
|
54
|
+
if (ka && kb)
|
|
55
|
+
return `${fg(ka)}${bg(kb)}▀\x1b[0m`;
|
|
56
|
+
if (ka)
|
|
57
|
+
return `${fg(ka)}▀\x1b[0m`;
|
|
58
|
+
return `${fg(kb)}▄\x1b[0m`;
|
|
59
|
+
}
|
|
60
|
+
// Six 16-wide rows. Pure: unit-testable without a TTY.
|
|
61
|
+
function bannerLines(tier = bannerTier()) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (let r = 0; r < GRID.length; r += 2) {
|
|
64
|
+
let line = '';
|
|
65
|
+
for (let x = 0; x < GRID[r].length; x++) {
|
|
66
|
+
const ka = GRID[r][x] === '.' ? null : GRID[r][x];
|
|
67
|
+
const kb = GRID[r + 1][x] === '.' ? null : GRID[r + 1][x];
|
|
68
|
+
line += paint(ka, kb, tier);
|
|
69
|
+
}
|
|
70
|
+
out.push(line);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
let shown = false;
|
|
75
|
+
// OMP-style welcome: intro line + note box with art and tips.
|
|
76
|
+
// No-op unless interactive (and never on --dry-run).
|
|
77
|
+
function printWelcome() {
|
|
78
|
+
if (shown || dryRun || !tty())
|
|
79
|
+
return;
|
|
80
|
+
shown = true;
|
|
81
|
+
clackIntro(`✂ tersio v${PACKAGE_VERSION}`);
|
|
82
|
+
clackNote(`${bannerLines().join('\n')}\n\n tersio install set up add-ons\n tersio update refresh CLI + add-ons\n tersio doctor verify the install`, 'cut token bloat');
|
|
83
|
+
}
|
|
84
|
+
export { bannerLines, bannerTier, printWelcome };
|
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 = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, version: true, help: true };
|
|
49
|
+
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, reset: true, version: true, help: true };
|
|
50
50
|
const args = process.argv.slice(2);
|
|
51
51
|
const commandArg = args.find((arg, index) => !arg.startsWith('-') && args[index - 1] !== '--scope');
|
|
52
52
|
const command = commandArg?.toLowerCase() || null;
|
|
@@ -63,6 +63,7 @@ const verbose = args.includes('--verbose');
|
|
|
63
63
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
64
64
|
const usage = command === 'usage' || args.includes('--usage');
|
|
65
65
|
const gain = command === 'gain';
|
|
66
|
+
const reset = command === 'reset';
|
|
66
67
|
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
67
68
|
const dashboardOpen = args.includes('--open');
|
|
68
69
|
const dashboardExport = flagValue('--export') ?? null;
|
|
@@ -254,4 +255,4 @@ function relTime(ageMs) {
|
|
|
254
255
|
const weeks = Math.floor(days / 7);
|
|
255
256
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
256
257
|
}
|
|
257
|
-
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage, gain, dashboardPort, dashboardOpen, dashboardExport, removePonytail, keepPonytail, removeRtk, scopeFlag, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, SCOPE_MAP, relTime, };
|
|
258
|
+
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage, gain, reset, dashboardPort, dashboardOpen, dashboardExport, removePonytail, keepPonytail, removeRtk, scopeFlag, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, SCOPE_MAP, relTime, };
|
package/cli/dashboard.js
CHANGED
|
@@ -8,13 +8,16 @@ import { promises as fs } from 'node:fs';
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { readUsage } from '../extensions/shared/usage-ledger.js';
|
|
11
|
+
import { clearUsageLedger, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
12
12
|
import { summarizeUsage } from './usage.js';
|
|
13
13
|
import { withInteractiveSpinner } from './interactive.js';
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const DASH_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard');
|
|
15
|
+
const TEMPLATE = path.join(DASH_DIR, 'template.html');
|
|
16
|
+
const STYLES = path.join(DASH_DIR, 'styles.css');
|
|
17
|
+
const APP = path.join(DASH_DIR, 'app.js');
|
|
18
|
+
const BRAND = path.join(DASH_DIR, 'brand.webp');
|
|
19
|
+
async function readSegment(file) {
|
|
20
|
+
return fs.readFile(file, 'utf8');
|
|
18
21
|
}
|
|
19
22
|
async function faviconDataUri() {
|
|
20
23
|
const png = await fs.readFile(BRAND);
|
|
@@ -29,22 +32,45 @@ function openBrowser(url) {
|
|
|
29
32
|
}
|
|
30
33
|
async function runDashboard(options) {
|
|
31
34
|
if (options.exportFile) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const [template, css, js, icon] = await Promise.all([
|
|
36
|
+
readSegment(TEMPLATE), readSegment(STYLES), readSegment(APP), faviconDataUri(),
|
|
37
|
+
]);
|
|
38
|
+
// Replacer functions throughout: session data routinely contains `$'`
|
|
39
|
+
// sequences (shell quoting in tool details), which String.replace would
|
|
40
|
+
// expand as match-suffix patterns and corrupt the file.
|
|
41
|
+
const inline = template
|
|
42
|
+
.replace('<link rel="stylesheet" href="styles.css">', () => `<style>\n${css}</style>`)
|
|
43
|
+
.replace('<script src="app.js" defer></script>', () => `<script>\n${js}</script>`)
|
|
44
|
+
.replace("fetch('data.json')", () => `Promise.resolve({ json: function () { return ${dataJson()}; } })`)
|
|
45
|
+
.replace('href="brand.webp"', () => `href="${icon}"`)
|
|
46
|
+
.replace('src="brand.webp"', () => `src="${icon}"`);
|
|
37
47
|
await fs.writeFile(options.exportFile, inline, 'utf8');
|
|
38
48
|
console.log(`[ok] gain exported → ${options.exportFile}`);
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
41
|
-
const html = await withInteractiveSpinner('Loading dashboard template',
|
|
51
|
+
const html = await withInteractiveSpinner('Loading dashboard template', () => readSegment(TEMPLATE));
|
|
42
52
|
const server = http.createServer(async (req, res) => {
|
|
53
|
+
if (req.url === '/reset' && req.method === 'POST') {
|
|
54
|
+
const rows = clearUsageLedger();
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
56
|
+
res.end(JSON.stringify({ ok: true, rows }));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
43
59
|
if (req.url === '/data.json') {
|
|
44
60
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
45
61
|
res.end(dataJson());
|
|
46
62
|
return;
|
|
47
63
|
}
|
|
64
|
+
if (req.url === '/styles.css') {
|
|
65
|
+
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' });
|
|
66
|
+
res.end(await readSegment(STYLES));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (req.url === '/app.js') {
|
|
70
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8' });
|
|
71
|
+
res.end(await readSegment(APP));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
48
74
|
if (req.url === '/brand.webp') {
|
|
49
75
|
try {
|
|
50
76
|
const png = await fs.readFile(BRAND);
|
package/cli/doctor.js
CHANGED
|
@@ -3,7 +3,8 @@ 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
|
+
import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
7
|
+
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
7
8
|
import { checkForUpdate } from './update.js';
|
|
8
9
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
9
10
|
async function runDoctor() {
|
|
@@ -133,6 +134,12 @@ async function runDoctor() {
|
|
|
133
134
|
section('Usage');
|
|
134
135
|
const usageRows = readUsage();
|
|
135
136
|
check('Usage ledger', usageRows.length > 0, usageRows.length ? `${usageRows.length} rows · ${ledgerPath()}` : ledgerPath());
|
|
137
|
+
section('Records');
|
|
138
|
+
console.log(` Usage ledger (tersio-owned, tersio reset clears): ${ledgerPath()} · ${usageRows.length} rows`);
|
|
139
|
+
console.log(` Session transcripts (host-owned, never touched): ${sessionsDir()}`);
|
|
140
|
+
const rtkDb = rtkDbPath();
|
|
141
|
+
const rtk = readRtkGain();
|
|
142
|
+
console.log(` RTK history (rtk-owned, never touched): ${rtkDb}${rtk.commands ? ` · ${rtk.commands} commands` : ''}`);
|
|
136
143
|
section('Add-ons');
|
|
137
144
|
const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
|
|
138
145
|
check('Caveman rule', cavemanRuleText !== null, ruleAge);
|
package/cli/install.js
CHANGED
|
@@ -5,9 +5,11 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { BUN_BIN_DIR, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, SCOPE_MAP, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, scopeFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
7
|
import { ask, askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
|
|
8
|
+
import { printWelcome } from './banner.js';
|
|
8
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
9
10
|
import { runUninstall } from './uninstall.js';
|
|
10
11
|
import { runDoctor } from './doctor.js';
|
|
12
|
+
import { runReset } from './reset.js';
|
|
11
13
|
import { runUsage } from './usage.js';
|
|
12
14
|
import { runDashboard } from './dashboard.js';
|
|
13
15
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
|
|
@@ -549,6 +551,7 @@ let updatePromptDone = false;
|
|
|
549
551
|
// update offer first (when pending), then a Clack menu over every command.
|
|
550
552
|
// Scripts, pipes, --yes, and --dry-run keep the old straight-to-install path.
|
|
551
553
|
async function runCommandMenu() {
|
|
554
|
+
printWelcome();
|
|
552
555
|
const newer = await checkForUpdate();
|
|
553
556
|
if (typeof newer === 'string' && !dryRun) {
|
|
554
557
|
const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
|
|
@@ -566,11 +569,11 @@ async function runCommandMenu() {
|
|
|
566
569
|
updatePromptDone = true;
|
|
567
570
|
const choice = await askInteractiveChoice('Tersio — what next?', [
|
|
568
571
|
{ value: 'install', label: 'Install add-ons', hint: 'user/project scope + combo defaults' },
|
|
569
|
-
{ value: 'check', label: 'Check for updates', hint: '
|
|
570
|
-
{ value: 'update', label: 'Update everything', hint: 'CLI plus all ai-addons' },
|
|
572
|
+
{ value: 'check', label: 'Check for updates', hint: 'check CLI version, then update ai-addons' },
|
|
571
573
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
572
574
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
573
575
|
{ value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
|
|
576
|
+
{ value: 'reset', label: 'Reset statistics', hint: 'clear the tersio usage ledger' },
|
|
574
577
|
{ value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
|
|
575
578
|
], 'install');
|
|
576
579
|
if (choice.status !== 'selected') {
|
|
@@ -582,6 +585,8 @@ async function runCommandMenu() {
|
|
|
582
585
|
await runInstall();
|
|
583
586
|
break;
|
|
584
587
|
case 'check': {
|
|
588
|
+
// One bound flow: report the CLI version, then offer the ai-addons
|
|
589
|
+
// refresh in the same pass — no redundant second "update" option.
|
|
585
590
|
const latest = await checkForUpdate(true);
|
|
586
591
|
if (typeof latest === 'string')
|
|
587
592
|
console.log(` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})`);
|
|
@@ -589,13 +594,20 @@ async function runCommandMenu() {
|
|
|
589
594
|
console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
|
|
590
595
|
else
|
|
591
596
|
console.log(` [ok] tersio ${PACKAGE_VERSION} is the latest`);
|
|
597
|
+
const go = await askInteractiveConfirm('Update ai-addons now (RTK, Caveman rule, Ponytail)?');
|
|
598
|
+
if (go.status === 'confirmed' && go.value) {
|
|
599
|
+
await runLatestUpdate();
|
|
600
|
+
}
|
|
601
|
+
else if (go.status === 'cancelled') {
|
|
602
|
+
closeRL();
|
|
603
|
+
process.exit(130);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
console.log(` staying on ${PACKAGE_VERSION} — run \`tersio update\` anytime`);
|
|
607
|
+
}
|
|
592
608
|
closeRL();
|
|
593
609
|
break;
|
|
594
610
|
}
|
|
595
|
-
case 'update':
|
|
596
|
-
await runLatestUpdate();
|
|
597
|
-
closeRL();
|
|
598
|
-
break;
|
|
599
611
|
case 'doctor':
|
|
600
612
|
await runDoctor();
|
|
601
613
|
closeRL();
|
|
@@ -608,6 +620,10 @@ async function runCommandMenu() {
|
|
|
608
620
|
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
|
|
609
621
|
closeRL();
|
|
610
622
|
break;
|
|
623
|
+
case 'reset':
|
|
624
|
+
await runReset();
|
|
625
|
+
closeRL();
|
|
626
|
+
break;
|
|
611
627
|
case 'uninstall':
|
|
612
628
|
await runUninstall();
|
|
613
629
|
closeRL();
|
|
@@ -615,10 +631,14 @@ async function runCommandMenu() {
|
|
|
615
631
|
}
|
|
616
632
|
}
|
|
617
633
|
async function runInstall() {
|
|
618
|
-
|
|
634
|
+
// Menu-driven installs must fall through: bare `tersio` re-enters here
|
|
635
|
+
// with command === null after the picker, and without this guard the
|
|
636
|
+
// choice loops straight back into runCommandMenu() forever.
|
|
637
|
+
if (command === null && tty() && !yes && !updatePromptDone) {
|
|
619
638
|
await runCommandMenu();
|
|
620
639
|
return;
|
|
621
640
|
}
|
|
641
|
+
printWelcome();
|
|
622
642
|
if (reinstall) {
|
|
623
643
|
await runUninstall({ yes: true, removePonytail: false, removeRtk: true });
|
|
624
644
|
}
|
package/cli/reset.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// cli/reset.ts — clear tersio-owned statistics (usage ledger).
|
|
2
|
+
// Session transcripts (~/.omp/agent/sessions) and the RTK database are
|
|
3
|
+
// host/tool-owned and are never touched; doctor shows where they live.
|
|
4
|
+
import { cancel as clackCancel, confirm as clackConfirm } from '@clack/prompts';
|
|
5
|
+
import { dryRun, yes } from './common.js';
|
|
6
|
+
import { ask, closeRL, tty } from './interactive.js';
|
|
7
|
+
import { clearUsageLedger, ledgerPath, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
8
|
+
import { sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
9
|
+
import { rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
10
|
+
async function runReset() {
|
|
11
|
+
console.log('\n=== Tersio Reset ===\n');
|
|
12
|
+
const rows = readUsage().length;
|
|
13
|
+
console.log(`Will remove: ${rows} usage rows · ${ledgerPath()}`);
|
|
14
|
+
console.log(`Left intact: sessions ${sessionsDir()}, rtk ${rtkDbPath()}`);
|
|
15
|
+
if (dryRun) {
|
|
16
|
+
console.log('[dry-run] ledger kept.');
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (rows === 0) {
|
|
20
|
+
console.log('Nothing to reset — usage ledger is already empty.');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (!yes) {
|
|
24
|
+
if (tty()) {
|
|
25
|
+
closeRL();
|
|
26
|
+
const confirmedChoice = await clackConfirm({ message: 'Clear the usage ledger?', initialValue: false });
|
|
27
|
+
if (typeof confirmedChoice !== 'boolean') {
|
|
28
|
+
clackCancel('Aborted.');
|
|
29
|
+
closeRL();
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!confirmedChoice) {
|
|
33
|
+
console.log('Aborted.');
|
|
34
|
+
closeRL();
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const answer = await ask('\nProceed? [y/N]: ');
|
|
40
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
41
|
+
console.log('Aborted.');
|
|
42
|
+
closeRL();
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const cleared = clearUsageLedger();
|
|
48
|
+
console.log(`[ok] reset — removed ${cleared} usage rows`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
export { runReset };
|
package/cli/update.js
CHANGED
|
@@ -5,6 +5,7 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { BUN_BIN_DIR, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, SCOPE_MAP, dryRun, execP, parseJsonObject, scopeFlag, verbose, } from './common.js';
|
|
6
6
|
import { execNetwork } from './interactive.js';
|
|
7
7
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
|
+
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
8
9
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
|
|
9
10
|
const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
10
11
|
function newerThan(a, b) {
|
|
@@ -195,6 +196,13 @@ async function runLatestUpdate() {
|
|
|
195
196
|
try {
|
|
196
197
|
await execInherit(npmCommand, npmCommandArgs);
|
|
197
198
|
console.log('\n=== Update complete ===');
|
|
199
|
+
if (!dryRun) {
|
|
200
|
+
try {
|
|
201
|
+
if (await refreshPrices())
|
|
202
|
+
console.log(' [ok] model price table refreshed');
|
|
203
|
+
}
|
|
204
|
+
catch { /* pricing is best-effort; update already succeeded */ }
|
|
205
|
+
}
|
|
198
206
|
}
|
|
199
207
|
catch (e) {
|
|
200
208
|
const err = e;
|