@krtclcdy/tersio 2.12.0 → 2.13.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 CHANGED
@@ -1,4 +1,4 @@
1
- ![Fontora](/TersioBanner.webp)
1
+ ![Banner](/TersioBanner.webp)
2
2
 
3
3
  # ✂️ Tersio — Token-saving OMP Add-ons
4
4
 
@@ -13,7 +13,7 @@ Terse replies, compact shell output, and minimal code decisions for [Oh My Pi (O
13
13
  ## ✨ Features
14
14
 
15
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.
16
+ - **RTK** — Routes noisy shell commands through the RTK binary for compact output; exact bytes bypass it by policy. The installer wires rtk into OMP automatically (`rtk init -g --agent omp`), so bash tool calls rewrite to `rtk` before execution and meter into the gain dashboard.
17
17
  - **Ponytail** — Minimal, YAGNI-oriented code decisions, from nudge (`lite`) to requirement-challenger (`ultra`).
18
18
  - **Combo** — Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max`.
19
19
  - **Updater** — `/tersio check` and `/tersio update` cover Ponytail, RTK, and Caveman in-session, with dry-run and backups.
@@ -84,14 +84,19 @@ Measured savings against the same workload without the modes. Token counts are r
84
84
 
85
85
  | Measured surface (n) | Baseline → Tersio | Δ |
86
86
  |---|---|---|
87
- | Terse reply, caveman lite — BPE tok (3 samples, p50) | 146 → 73 tok | **−52.6%** |
88
- | Terse reply, caveman full — BPE tok (3 samples, p50) | 154 → 58 tok | **−62.3%** |
89
- | Terse reply, caveman ultra — BPE tok (3 samples, p50) | 146 → 33 tok | **−78.6%** |
90
- | Code diff, ponytail minimal-ladder — BPE tok (2 tasks) | 261–481 → 108–143 tok | **−58.6…−70.3%** |
91
- | Shell output, rtk — `git status` — BPE tok | 32 → 21 tok | **−34.4%** |
92
- | Shell output, rtk — repo `grep` — BPE tok | 537 → 469 tok | **−12.7%** |
93
-
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.
87
+ | Terse reply, caveman lite — BPE tok (3 samples, p50) | 173 → 69 tok | **−60.1%** |
88
+ | Terse reply, caveman full — BPE tok (3 samples, p50) | 173 → 49 tok | **−71.7%** |
89
+ | Terse reply, caveman ultra — BPE tok (3 samples, p50) | 173 → 25 tok | **−85.5%** |
90
+ | Code diff, ponytail lite — BPE tok (2 tasks) | 326–462 → 57–121 tok | **−73.8…−82.5%** |
91
+ | Code diff, ponytail full — BPE tok (2 tasks) | 326–462 → 55–88 tok | **−81.0…−83.1%** |
92
+ | Code diff, ponytail ultra — BPE tok (2 tasks) | 326–462 → 40–88 tok | **−81.0…−87.7%** |
93
+ | Shell output, rtk — `git status` — BPE tok | 182 → 72 tok | **−60.4%** |
94
+ | Shell output, rtk — repo `grep` — BPE tok | 376 → 274 tok | **−27.1%** |
95
+ | Shell output, rtk — `find` — BPE tok | 205 → 168 tok | **−18.0%** |
96
+ | Shell output, rtk — `git diff` — BPE tok | 12,684 → 8,504 tok | **−33.0%** |
97
+ | Shell output, rtk — `npm test` (passthrough) — BPE tok | 2,597 → 2,577 tok | **−0.8%** |
98
+
99
+ 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).
95
100
 
96
101
  ## 🖥️ CLI
97
102
 
@@ -104,7 +109,7 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead
104
109
  | `tersio usage` | Ledger-backed usage + savings report |
105
110
  | `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`; serves localhost only) |
106
111
  | `tersio reset` | Clear tersio statistics (usage ledger; `--dry-run`, `--yes`) — sessions and RTK history stay intact |
107
- | `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
112
+ | `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary and its `rtk.ts` OMP wiring) |
108
113
  | `tersio version` | Print version |
109
114
 
110
115
  Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`. Legacy `--doctor` / `--uninstall` forms still work.
@@ -132,6 +137,7 @@ Everything runs under one root. Bare `/tersio` prints status.
132
137
  | What | Path |
133
138
  |---|---|
134
139
  | Caveman / RTK / Updater / Combo extensions | `~/.omp/agent/extensions/{caveman-session,rtk-session,ai-addons-updater,combo-toggle}/` |
140
+ | 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 |
135
141
  | Ponytail package | `~/.omp/plugins/node_modules/@dietrichgebert/ponytail/` |
136
142
  | RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows) |
137
143
  | Extension registrations | `~/.omp/agent/config.yml` |
@@ -146,6 +152,8 @@ User-level installs also register the package in `~/.omp/plugins` (visible in OM
146
152
 
147
153
  **RTK missing or not executable:** run `tersio reinstall`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
148
154
 
155
+ **RTK commands not metered in the gain 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.
156
+
149
157
  **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.
150
158
 
151
159
  ## 🤝🏻 Contributing
package/cli/doctor.js CHANGED
@@ -39,6 +39,7 @@ async function runDoctor() {
39
39
  pluginsPkgRaw: readTextIfExists(path.join(pluginsDir, 'package.json')),
40
40
  selfPkgText: readTextIfExists(selfPkg),
41
41
  rtkBinText: readTextIfExists(rtkBin),
42
+ rtkOmpText: readTextIfExists(path.join(extDir, 'rtk.ts')),
42
43
  cavemanIndexText: readTextIfExists(cavemanIndex),
43
44
  cavemanRuleText: readTextIfExists(cavemanRule),
44
45
  rtkIndexText: readTextIfExists(rtkIndex),
@@ -75,12 +76,13 @@ async function runDoctor() {
75
76
  probes.pluginsPkgRaw,
76
77
  probes.selfPkgText,
77
78
  ]));
78
- const [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime] = await runInteractivePhase('Checking add-ons', () => Promise.all([
79
+ const [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime] = await runInteractivePhase('Checking add-ons', () => Promise.all([
79
80
  probes.cavemanRuleText,
80
81
  probes.ruleMtime,
81
82
  probes.rtkBinText,
82
83
  probes.rtkMtime,
83
84
  rtkVersionProbe,
85
+ probes.rtkOmpText,
84
86
  probes.ponytailMtime,
85
87
  ]));
86
88
  // Categorized output with a tally; section headers group related probes.
@@ -151,6 +153,10 @@ async function runDoctor() {
151
153
  check('RTK binary', true, bits);
152
154
  if (!rtkVersion)
153
155
  warnLine('RTK version', 'unavailable — binary may not be executable');
156
+ if (rtkOmpText === null)
157
+ check('RTK OMP wiring (rtk.ts)', false, `run: rtk init -g --agent omp`);
158
+ else
159
+ check('RTK OMP wiring (rtk.ts)', true, path.join(extDir, 'rtk.ts'));
154
160
  }
155
161
  const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
156
162
  check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
package/cli/install.js CHANGED
@@ -12,6 +12,7 @@ import { runDoctor } from './doctor.js';
12
12
  import { runReset } from './reset.js';
13
13
  import { runUsage } from './usage.js';
14
14
  import { runDashboard } from './dashboard.js';
15
+ import { wireRtkOmp } from './rtk-wiring.js';
15
16
  import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
16
17
  // Paths to extension source files (relative to this script)
17
18
  const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
@@ -234,6 +235,15 @@ async function verifyRtkArchive(archivePath, assetName, checksumsText) {
234
235
  function shortError(e) {
235
236
  return ((e.stderr) || e.message || '').trim().slice(0, 200);
236
237
  }
238
+ async function fileExists(p) {
239
+ try {
240
+ await fs.access(p);
241
+ return true;
242
+ }
243
+ catch {
244
+ return false;
245
+ }
246
+ }
237
247
  async function extractRtkArchive(archivePath, extractDir) {
238
248
  await fs.mkdir(extractDir, { recursive: true });
239
249
  if (archivePath.endsWith('.zip')) {
@@ -263,6 +273,9 @@ async function extractRtkArchive(archivePath, extractDir) {
263
273
  }
264
274
  async function stepRtk(binDir, options) {
265
275
  console.log('\n[3/8] Installing RTK binary...');
276
+ const binDest = path.join(binDir, RTK_BINARY_NAME);
277
+ // Download failure must not skip wiring: a pre-existing rtk binary is
278
+ // exactly as good for the OMP hook, so wire whatever ends up at binDest.
266
279
  try {
267
280
  const release = await withInteractiveSpinner('Finding latest RTK release', () => fetchJson(RTK_RELEASE_API));
268
281
  const triple = resolveRtkTriple();
@@ -271,7 +284,6 @@ async function stepRtk(binDir, options) {
271
284
  const asset = findRtkAsset(release, triple);
272
285
  if (!asset)
273
286
  return;
274
- const binDest = path.join(binDir, RTK_BINARY_NAME);
275
287
  if (options.dryRun) {
276
288
  console.log(` [dry-run] would download ${asset.name} from release ${release.tag_name}`);
277
289
  console.log(' [dry-run] would verify checksum against checksums.txt');
@@ -308,7 +320,7 @@ async function stepRtk(binDir, options) {
308
320
  debug(`chmod 755 ${binDest}`);
309
321
  }
310
322
  try {
311
- const v = (await execP(binDest, ['--version'], { timeout: 10000, shell: false })).stdout.trim();
323
+ const v = (await execP(binDest, ['--version'], { timeout: 30000, shell: false })).stdout.trim();
312
324
  console.log(` [ok] ${binDest} → ${v}`);
313
325
  }
314
326
  catch {
@@ -323,6 +335,17 @@ async function stepRtk(binDir, options) {
323
335
  console.log(` [fail] RTK: ${e.message}`);
324
336
  console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
325
337
  }
338
+ if (options.dryRun) {
339
+ await wireRtkOmp(binDest, options);
340
+ return;
341
+ }
342
+ if (!(await fileExists(binDest))) {
343
+ console.log(' [skip] no rtk binary to wire — install rtk, then run: rtk init -g --agent omp');
344
+ return;
345
+ }
346
+ // Binary alone never meters OMP sessions — wire rtk's tool_call
347
+ // extension so bash commands rewrite to rtk and land in history.db.
348
+ await wireRtkOmp(binDest, options);
326
349
  }
327
350
  // Copy repo source files into the target extension dir. First entry is
328
351
  // required (skip label on missing); rest are optional companions.
@@ -0,0 +1,58 @@
1
+ // cli/rtk-wiring.ts — wire the installed rtk binary into OMP.
2
+ // `rtk init -g --agent omp` writes rtk's tool_call extension to
3
+ // ~/.omp/agent/extensions/rtk.ts; OMP auto-loads it and rewrites bash
4
+ // commands to rtk before execution, so every rewritten call lands in
5
+ // rtk's history.db and shows metered savings in the gain dashboard.
6
+ // Fail-open: a failed wire only downgrades to manual `rtk` prefixing.
7
+ // Deliberately free of cli/common.ts imports (argv side effects) so tests
8
+ // can load it directly.
9
+ import { execFile } from 'node:child_process';
10
+ function shortWiringError(e) {
11
+ const err = e;
12
+ const why = [err.code !== undefined ? `code ${err.code}` : '', err.signal ?? '', err.stderr?.trim(), err.message]
13
+ .filter(Boolean).join(' · ');
14
+ return why.slice(0, 200);
15
+ }
16
+ function execFileP(cmd, args, timeout) {
17
+ return new Promise((resolve, reject) => {
18
+ execFile(cmd, args, { timeout, encoding: 'utf8' }, (err, stdout, stderr) => {
19
+ if (err) {
20
+ err.stderr = String(stderr || '');
21
+ reject(err);
22
+ }
23
+ else {
24
+ resolve({ stdout: String(stdout), stderr: String(stderr) });
25
+ }
26
+ });
27
+ });
28
+ }
29
+ // Idempotent: rtk rewrites its extension file on every init run, so
30
+ // reinstalling tersio refreshes the wiring for free.
31
+ export async function wireRtkOmp(rtkBin, options = {}) {
32
+ console.log(' Wiring rtk → OMP (bash tool_call rewrite)...');
33
+ if (options.dryRun) {
34
+ console.log(' [dry-run] would run: rtk init -g --agent omp');
35
+ return true;
36
+ }
37
+ try {
38
+ await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
39
+ console.log(' [ok] rtk OMP extension wired (~/.omp/agent/extensions/rtk.ts)');
40
+ return true;
41
+ }
42
+ catch (first) {
43
+ // A freshly written binary can lose its first exec to macOS Gatekeeper
44
+ // verification; give it one settle-and-retry before reporting failure.
45
+ await new Promise((resolve) => setTimeout(resolve, 2000));
46
+ try {
47
+ await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
48
+ console.log(' [ok] rtk OMP extension wired (~/.omp/agent/extensions/rtk.ts)');
49
+ return true;
50
+ }
51
+ catch (e) {
52
+ void first;
53
+ console.log(` [warn] could not wire rtk → OMP: ${shortWiringError(e)}`);
54
+ console.log(' [hint] Manual: rtk init -g --agent omp (needs rtk >= 0.49)');
55
+ return false;
56
+ }
57
+ }
58
+ }
package/cli/uninstall.js CHANGED
@@ -88,6 +88,7 @@ async function runUninstall(options = {}) {
88
88
  }
89
89
  if (shouldRemoveRtk) {
90
90
  console.log(` ${rtkBin}`);
91
+ console.log(` ${path.join(extDir, 'rtk.ts')} (rtk OMP wiring)`);
91
92
  }
92
93
  if (!confirmed) {
93
94
  if (tty()) {
@@ -162,9 +163,13 @@ async function runUninstall(options = {}) {
162
163
  if ((await readTextIfExists(path.join(selfPluginDir, 'package.json'))) !== null) {
163
164
  await removeUninstallTarget(selfPluginDir, shouldDryRun);
164
165
  }
165
- // Remove RTK binary if requested
166
- if (shouldRemoveRtk)
166
+ // Remove RTK binary if requested. rtk.ts is rtk-owned but installed by our
167
+ // wiring step; with the binary gone it would pass through harmlessly, so
168
+ // only drop it on a full rtk removal.
169
+ if (shouldRemoveRtk) {
167
170
  await removeUninstallTarget(rtkBin, shouldDryRun, false);
171
+ await removeUninstallTarget(path.join(extDir, 'rtk.ts'), shouldDryRun);
172
+ }
168
173
  console.log('\nDone. Restart OMP for changes to take effect.');
169
174
  return true;
170
175
  }
@@ -5,6 +5,7 @@
5
5
  --cell-0: rgba(24, 24, 27, .07);
6
6
  --cell-1: rgba(4, 120, 87, .3); --cell-2: rgba(4, 120, 87, .55);
7
7
  --cell-3: rgba(4, 120, 87, .8); --cell-4: #047857;
8
+ --danger: #dc2626; --danger-border: rgba(220, 38, 38, .35); --danger-soft: rgba(220, 38, 38, .05);
8
9
  --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.07);
9
10
  }
10
11
  html[data-theme="light"] { color-scheme: light; }
@@ -17,6 +18,7 @@
17
18
  --cell-0: rgba(255, 255, 255, .07);
18
19
  --cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
19
20
  --cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
21
+ --danger: #f87171; --danger-border: rgba(248, 113, 113, .3); --danger-soft: rgba(248, 113, 113, .07);
20
22
  --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.55);
21
23
  }
22
24
  }
@@ -36,6 +38,7 @@
36
38
  --cell-0: rgba(255, 255, 255, .07);
37
39
  --cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
38
40
  --cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
41
+ --danger: #f87171; --danger-border: rgba(248, 113, 113, .3); --danger-soft: rgba(248, 113, 113, .07);
39
42
  --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.55);
40
43
  }
41
44
  .ticker { animation: tick 36s linear infinite; will-change: transform; }
@@ -185,6 +188,7 @@
185
188
  .dlg-desc { font-size: 12px; color: var(--dim); margin: 2px 0 0; }
186
189
  .dlg-label { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); margin: 18px 0 8px; }
187
190
  .dlg-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
191
+ .dlg-row + .dlg-row { margin-top: 14px; }
188
192
  .dlg-name { font-size: 13px; font-weight: 600; margin: 0; }
189
193
  .dlg-hint { font-size: 12px; color: var(--dim); margin: 2px 0 0; }
190
194
  .dlg-own { font-weight: 400; font-size: 11px; color: var(--dim); }
@@ -192,4 +196,8 @@
192
196
  .dlg-path { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; min-width: 0; overflow: hidden; }
193
197
  .dlg-file { font-size: 11px; color: var(--dim); margin: 4px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
194
198
  #settingsBtn:hover { box-shadow: 0 0 14px var(--accent-soft); border-color: var(--accent); }
199
+ .dlg-danger { margin-top: 16px; border: 1px solid var(--danger-border); background: var(--danger-soft); border-radius: 12px; padding: 12px; }
200
+ .dlg-danger .dlg-label { color: var(--danger); margin: 0 0 10px; }
201
+ #reset { color: var(--danger); }
202
+ #reset:hover { border-color: var(--danger); box-shadow: 0 0 14px var(--danger-border); }
195
203
  .dlg .seg button { display: grid; place-items: center; }
@@ -36,9 +36,6 @@
36
36
  <span id="clock">--:--</span>
37
37
  </span>
38
38
  <span class="hidden sm:block w-px h-5 shrink-0" style="background: var(--line)" aria-hidden="true"></span>
39
- <button id="reset" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reset tersio statistics">
40
- <i data-lucide="rotate-ccw" class="size-3.5"></i><span class="hidden md:inline" id="resetLabel">Reset</span>
41
- </button>
42
39
  <button id="settingsBtn" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Open settings">
43
40
  <i data-lucide="settings" class="size-4"></i>
44
41
  </button>
@@ -49,7 +46,7 @@
49
46
  <div class="dlg-head">
50
47
  <div class="min-w-0">
51
48
  <p class="dlg-title" id="settingsTitle">Settings</p>
52
- <p class="dlg-desc">Appearance, data refresh, and where statistics live.</p>
49
+ <p class="dlg-desc">Appearance, data, and where statistics live.</p>
53
50
  </div>
54
51
  <button id="settingsClose" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Close settings">
55
52
  <i data-lucide="x" class="size-4"></i>
@@ -91,6 +88,18 @@
91
88
  <p class="mono dlg-file" id="pathRtk" title="">–</p>
92
89
  </div>
93
90
  </div>
91
+ <div class="dlg-danger">
92
+ <p class="dlg-label">Danger zone</p>
93
+ <div class="dlg-row">
94
+ <div class="min-w-0">
95
+ <p class="dlg-name">Reset statistics</p>
96
+ <p class="dlg-hint">Clears the usage ledger. Click twice to confirm.</p>
97
+ </div>
98
+ <button id="reset" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--danger-border)" aria-label="Reset tersio statistics">
99
+ <i data-lucide="rotate-ccw" class="size-3.5"></i><span id="resetLabel">Reset</span>
100
+ </button>
101
+ </div>
102
+ </div>
94
103
  </dialog>
95
104
 
96
105
  <!-- Hero: centered total -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.12.0",
3
+ "version": "2.13.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",