@krtclcdy/tersio 2.0.0 → 2.1.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
@@ -61,26 +61,28 @@ User-level installs also register the package in `~/.omp/plugins` (visible in OM
61
61
 
62
62
  ## Benchmarks
63
63
 
64
- Measured before/after — real `o200k_base` token counts, samples and run protocol in [BENCHMARK.md](./BENCHMARK.md). Overhead is paid once per session; savings accumulate per reply, command, or task.
64
+ 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).
65
65
 
66
- | Mode | Overhead (once) | Before | After | Saving | Pays off after |
67
- |---|---:|---:|---:|---:|---|
68
- | `/caveman lite` | 46 tok | 146 tok/reply | 67 | −55% | 1 reply |
69
- | `/caveman full` | 172 tok | 146 tok/reply | 61 | −58% | ~2 replies |
70
- | `/caveman ultra` | 68 tok | 146 tok/reply | 34 | −77% | 1 reply |
71
- | `/rtk on` | 165 tok | 537 tok (grep) | 469 | −13% (−34% on `git status`) | varies |
72
- | `/ponytail full` | 76 tok | 261–481 tok/task | 108–143 | −59 to −70% | first task |
73
- | `/combo balanced` | 480 tok | 944 tok mixed turn | 638 | −32%/turn | first turn |
66
+ | Measured surface (n) | Baseline → Tersio | Δ |
67
+ |---|---|---|
68
+ | Terse reply, caveman lite — BPE tok (3 samples, p50) | 146 → 73 tok | **−52.6%** |
69
+ | Terse reply, caveman full — BPE tok (3 samples, p50) | 154 → 58 tok | **−62.3%** |
70
+ | Terse reply, caveman ultra — BPE tok (3 samples, p50) | 146 → 33 tok | **−78.6%** |
71
+ | Code diff, ponytail minimal-ladder — BPE tok (2 tasks) | 261–481 → 108–143 tok | **−58.6…−70.3%** |
72
+ | Shell output, rtk — `git status` — BPE tok | 32 → 21 tok | **−34.4%** |
73
+ | Shell output, rtk — repo `grep` — BPE tok | 537 → 469 tok | **−12.7%** |
74
+
75
+ 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.
74
76
 
75
77
  ## CLI
76
78
 
77
79
  | Command | Purpose |
78
80
  |---|---|
79
81
  | `tersio install` | Install (user scope by default; `--scope project\|both` for more) |
80
- | `tersio update` | Refresh from the latest release |
82
+ | `tersio update` | Refresh the CLI, extensions, and add-ons (RTK binary, Caveman rule, Ponytail) |
81
83
  | `tersio reinstall` | Fresh install, preserving the Ponytail package |
82
84
  | `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
83
- | `tersio uninstall` | Remove extensions and registration (`--remove-rtk`, `--remove-ponytail` for more) |
85
+ | `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
84
86
  | `tersio version` | Print version |
85
87
 
86
88
  Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--scope`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`. Legacy `--doctor` / `--uninstall` forms still work.
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { dirname, join } from 'node:path';
4
- import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
4
+ import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
5
5
  import { readCavemanDefault } from '../shared/plugin-settings.js';
6
6
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
7
7
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
@@ -96,6 +96,11 @@ export default function cavemanSessionExtension(pi) {
96
96
  });
97
97
  function restoreMode(ctx) {
98
98
  const entries = sessionEntries(ctx);
99
+ // Publish the persisted combo state (incl. combo-level) before painting:
100
+ // bar suppression reads the in-process bridge, which is empty in a fresh
101
+ // host until the combo extension reconciles — and its reconcile is
102
+ // UI-gated. Deriving it here makes suppression independent of load order.
103
+ reconcileSharedComboEntries(entries);
99
104
  // Persisted session state wins; a fresh session falls back to the
100
105
  // installer/user-configured default (off unless configured).
101
106
  const persisted = resolveMode(entries, '');
@@ -29,7 +29,6 @@ function loadPonytailInstructions(mode) {
29
29
  }
30
30
  export default function comboToggleExtension(pi) {
31
31
  pi.setLabel?.('Combo session toggle (all 3 add-ons)');
32
- let currentState = getSharedComboState();
33
32
  let lastCtx = undefined;
34
33
  function syncStatus(ctx) {
35
34
  if (ctx)
@@ -43,11 +42,14 @@ export default function comboToggleExtension(pi) {
43
42
  c.ui.setStatus('combo', undefined);
44
43
  return;
45
44
  }
45
+ // Read the live bridge, not cached extension state: sibling extensions
46
+ // reconcile it from persisted entries before this one runs.
47
+ const state = getSharedComboState();
46
48
  const theme = c.ui.theme;
47
- const c0 = currentState.caveman.toUpperCase();
48
- const r = currentState.rtk.toUpperCase();
49
- const p = currentState.ponytail.toUpperCase();
50
- const lvl = currentState.level.toUpperCase();
49
+ const c0 = state.caveman.toUpperCase();
50
+ const r = state.rtk.toUpperCase();
51
+ const p = state.ponytail.toUpperCase();
52
+ const lvl = state.level.toUpperCase();
51
53
  const label = `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`;
52
54
  c.ui.setStatus('combo', theme?.fg ? `${theme.fg('accent', '🧩')} ${theme.fg('muted', label)}` : `🧩 ${label}`);
53
55
  // Clobber any sibling bars another extension may have painted in a race
@@ -67,13 +69,12 @@ export default function comboToggleExtension(pi) {
67
69
  pi.appendEntry?.('combo-level', { level });
68
70
  }
69
71
  function useState(state, ctx) {
70
- currentState = state;
71
72
  syncStatus(ctx);
72
73
  return state;
73
74
  }
74
75
  function reconcile(ctx) {
75
76
  if (!ctx?.hasUI)
76
- return currentState;
77
+ return getSharedComboState();
77
78
  return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
78
79
  }
79
80
  function listen(ctx) {
@@ -109,7 +110,7 @@ export default function comboToggleExtension(pi) {
109
110
  }
110
111
  persistPreset(level);
111
112
  useState(setSharedComboLevel(level), ctx);
112
- ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(currentState)}`, 'info');
113
+ ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(getSharedComboState())}`, 'info');
113
114
  if (ctx?.reload)
114
115
  await ctx.reload();
115
116
  },
@@ -1,6 +1,6 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
3
+ import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
4
4
  import { readRtkDefault } from '../shared/plugin-settings.js';
5
5
  const DEFAULT_ENABLED = false;
6
6
  function asBoolean(value) {
@@ -102,6 +102,11 @@ export default function rtkSessionExtension(pi) {
102
102
  });
103
103
  function restoreEnabled(ctx) {
104
104
  const entries = sessionEntries(ctx);
105
+ // Publish the persisted combo state (incl. combo-level) before painting:
106
+ // bar suppression reads the in-process bridge, which is empty in a fresh
107
+ // host until the combo extension reconciles — and its reconcile is
108
+ // UI-gated. Deriving it here makes suppression independent of load order.
109
+ reconcileSharedComboEntries(entries);
105
110
  // Persisted session state wins; a fresh session falls back to the
106
111
  // installer/user-configured default (off unless configured).
107
112
  const persisted = resolveEnabled(entries);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.0.0",
3
+ "version": "2.1.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",
package/tersio.js CHANGED
@@ -69,6 +69,7 @@ const verbose = args.includes('--verbose');
69
69
  const doctor = command === 'doctor' || args.includes('--doctor');
70
70
  const uninstall = command === 'uninstall' || args.includes('--uninstall');
71
71
  const removePonytail = args.includes('--remove-ponytail');
72
+ const keepPonytail = args.includes('--keep-ponytail');
72
73
  const removeRtk = args.includes('--remove-rtk');
73
74
  const scopeFlag = flagValue('--scope')?.toLowerCase() ?? null;
74
75
  // --- Profile selection (session-start mode defaults) ---
@@ -83,7 +84,7 @@ function printHelp() {
83
84
 
84
85
  Commands:
85
86
  install Install the add-ons (user scope by default)
86
- update Run the latest published installer
87
+ update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail)
87
88
  reinstall Clean and reinstall the user-scope add-ons
88
89
  doctor Check the current installation
89
90
  uninstall Remove the managed extensions
@@ -92,7 +93,7 @@ Commands:
92
93
 
93
94
  Options:
94
95
  --scope user|project|both
95
- --remove-ponytail (uninstall: also remove the Ponytail plugin)
96
+ --keep-ponytail (uninstall: keep the Ponytail plugin — removed by default)
96
97
  --remove-rtk (uninstall: also remove the RTK binary)
97
98
  --combo-default off|medium|balanced|max (implies caveman, rtk, ponytail)
98
99
  --caveman-default off|lite|full|ultra|wenyan (override; default follows combo)
@@ -660,17 +661,8 @@ async function stepCombo(extDir, options) {
660
661
  await ensureExtensionInConfig(configPath, dest, 'combo', options);
661
662
  }
662
663
  // --- Doctor ---
663
- function presence(label, text) {
664
- console.log(` ${label}: ${text !== null ? 'installed' : 'MISSING'}`);
665
- }
666
- function locationLine(label, ok, loc) {
667
- console.log(` ${label}: ${ok ? 'ok' : 'MISSING'} ${loc}`);
668
- }
669
664
  async function runDoctor() {
670
- console.log('\n=== Tersio Doctor ===\n');
671
- // Node
672
- console.log(` Node: ok ${process.version}`);
673
- // OMP CLI version joins FS probes below; logs keep fixed order.
665
+ console.log('\n=== Tersio Doctor ===');
674
666
  // Directories
675
667
  const agentDir = OMP_AGENT_DIR;
676
668
  const extDir = path.join(agentDir, 'extensions');
@@ -706,48 +698,66 @@ async function runDoctor() {
706
698
  readTextIfExists(comboIndex),
707
699
  readTextIfExists(modeReinforcement),
708
700
  ]);
709
- console.log(ompVersion !== null ? ` OMP CLI: ok ${ompVersion}` : ' OMP CLI: MISSING');
710
- console.log(` Home: ${HOME}`);
711
- locationLine('OMP agent dir', agentEntries !== null, agentDir);
712
- locationLine('OMP extensions dir', extEntries !== null, extDir);
713
- console.log(` Shared session bridge: ${sharedStateText !== null ? 'installed' : 'MISSING'}`);
714
- locationLine('OMP config.yml', configText !== null, configPath);
715
- presence('Ponytail package', ponytailPkgText);
716
- presence('Ponytail extension', ponytailExtText);
717
- if (configText) {
718
- const hasPonytailPath = configText.includes('ponytail') && configText.includes('pi-extension');
719
- console.log(` Ponytail in config.yml: ${hasPonytailPath ? 'registered' : 'MISSING'}`);
720
- }
721
- // Self plugin registration (Settings → Plugins listing)
722
- const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
723
- console.log(` Self plugin in plugins/package.json: ${selfDep ? 'registered' : 'MISSING'}`);
724
- console.log(` Self plugin package: ${selfPkgText !== null ? 'installed' : 'MISSING'}`);
725
- // RTK
726
- const rtkExists = rtkBinText !== null;
727
- console.log(` RTK binary: ${rtkExists ? 'installed' : 'MISSING'} ${rtkBin}`);
728
- if (rtkExists) {
729
- try {
730
- const v = (await execP(rtkBin, ['--version'], { timeout: 5000 })).stdout.trim();
731
- console.log(` RTK version: ${v}`);
701
+ // Categorized output with a tally; section headers group related probes.
702
+ const tally = { ok: 0, missing: 0, warn: 0 };
703
+ function check(label, ok, detail = '') {
704
+ if (ok) {
705
+ tally.ok++;
706
+ console.log(` ${label}: ok${detail ? ` ${detail}` : ''}`);
732
707
  }
733
- catch {
734
- console.log(' RTK version: unavailable (may not be executable)');
708
+ else {
709
+ tally.missing++;
710
+ console.log(` ${label}: MISSING${detail ? ` ${detail}` : ''}`);
735
711
  }
736
712
  }
737
- // Caveman
738
- presence('Caveman extension', cavemanIndexText);
739
- presence('Caveman rule.md', cavemanRuleText);
740
- // RTK extension
741
- presence('RTK extension', rtkIndexText);
742
- // Updater
743
- presence('Updater extension', updaterIndexText);
744
- // Combo
745
- presence('Combo extension', comboIndexText);
746
- presence('Mode reinforcement extension', modeReinforcementText);
713
+ function warnLine(label, detail) {
714
+ tally.warn++;
715
+ console.log(` ${label}: warn ${detail}`);
716
+ }
717
+ section('Environment');
718
+ check('Node', true, process.version);
719
+ check('OMP CLI', ompVersion !== null, ompVersion ?? '');
720
+ console.log(` Home: ${HOME}`);
721
+ section('Installation');
722
+ check('OMP agent dir', agentEntries !== null, agentDir);
723
+ check('OMP extensions dir', extEntries !== null, extDir);
724
+ check('OMP config.yml', configText !== null, configPath);
725
+ check('Shared session bridge', sharedStateText !== null);
726
+ section('Extensions');
727
+ check('Caveman extension', cavemanIndexText !== null);
728
+ check('Caveman rule.md', cavemanRuleText !== null);
729
+ check('RTK extension', rtkIndexText !== null);
730
+ check('Updater extension', updaterIndexText !== null);
731
+ check('Combo extension', comboIndexText !== null);
732
+ check('Mode reinforcement extension', modeReinforcementText !== null);
733
+ section('Plugins');
734
+ check('Ponytail package', ponytailPkgText !== null);
735
+ check('Ponytail extension', ponytailExtText !== null);
747
736
  if (configText) {
748
- const hasComboPath = configText.includes('combo-toggle');
749
- console.log(` Combo in config.yml: ${hasComboPath ? 'registered' : 'MISSING'}`);
737
+ check('Ponytail in config.yml', configText.includes('ponytail') && configText.includes('pi-extension'));
738
+ check('Combo in config.yml', configText.includes('combo-toggle'));
750
739
  }
740
+ check('Self plugin package', selfPkgText !== null);
741
+ const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
742
+ check('Self plugin in plugins/package.json', selfDep);
743
+ section('RTK');
744
+ check('RTK binary', rtkBinText !== null, rtkBin);
745
+ if (rtkBinText !== null) {
746
+ // rtk may exit non-zero while still printing its version — accept either stream.
747
+ const version = await execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => r.stdout.trim() || r.stderr.trim() || null, (e) => {
748
+ const err = e;
749
+ return err.stdout?.trim() || err.stderr?.trim() || null;
750
+ });
751
+ if (version)
752
+ check('RTK version', true, version);
753
+ else
754
+ warnLine('RTK version', 'unavailable — binary may not be executable');
755
+ }
756
+ const total = tally.ok + tally.missing + tally.warn;
757
+ console.log(`\n Summary: ${total} checks — ${tally.ok} ok, ${tally.warn} warn, ${tally.missing} missing`);
758
+ }
759
+ function section(name) {
760
+ console.log(`\n${name}`);
751
761
  }
752
762
  // Read-modify-write a JSON file. mutate returns true when it changed
753
763
  // something; no change (or unreadable file) means no output at all.
@@ -800,7 +810,9 @@ async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
800
810
  async function runUninstall(options = {}) {
801
811
  const shouldDryRun = options.dryRun ?? dryRun;
802
812
  const confirmed = (options.yes ?? yes) || shouldDryRun;
803
- const shouldRemovePonytail = options.removePonytail ?? removePonytail;
813
+ // Ponytail ships with Tersio's presets, so a full uninstall removes it too;
814
+ // --keep-ponytail opts out and reinstall always preserves it.
815
+ const shouldRemovePonytail = options.removePonytail ?? (removePonytail || !keepPonytail);
804
816
  const shouldRemoveRtk = options.removeRtk ?? removeRtk;
805
817
  console.log('\n=== Tersio Uninstall ===\n');
806
818
  const extDir = path.join(OMP_AGENT_DIR, 'extensions');
@@ -894,6 +906,7 @@ async function runUninstall(options = {}) {
894
906
  return true;
895
907
  }
896
908
  const SCOPE_MAP = { user: '1', project: '2', both: '3' };
909
+ const COMBO_MENU = { '1': 'off', '2': 'medium', '3': 'balanced', '4': 'max' };
897
910
  async function runLatestUpdate() {
898
911
  const updateScope = scopeFlag || 'user';
899
912
  if (!Object.hasOwn(SCOPE_MAP, updateScope)) {
@@ -920,6 +933,31 @@ async function runLatestUpdate() {
920
933
  const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
921
934
  console.log('=== Updating Tersio ===');
922
935
  console.log(` Running the latest ${PACKAGE_NAME} installer...\n`);
936
+ // The npx delegation below only refreshes the OMP-side files; the globally
937
+ // installed CLI keeps its old version until npm -g runs. Refresh both.
938
+ if (dryRun) {
939
+ console.log(` [dry-run] would run: npm install -g ${PACKAGE_NAME}@latest --no-audit --no-fund`);
940
+ }
941
+ else {
942
+ try {
943
+ const globalArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}@latest`, '--no-audit', '--no-fund'] : ['install', '-g', `${PACKAGE_NAME}@latest`, '--no-audit', '--no-fund'];
944
+ const globalResult = await execP(npmCommand, globalArgs, {
945
+ timeout: 300000,
946
+ maxBuffer: 10 * 1024 * 1024,
947
+ windowsHide: true,
948
+ shell: false,
949
+ });
950
+ if (globalResult.stdout)
951
+ process.stdout.write(globalResult.stdout);
952
+ if (globalResult.stderr)
953
+ process.stderr.write(globalResult.stderr);
954
+ console.log(` [ok] CLI updated to ${PACKAGE_NAME}@latest`);
955
+ }
956
+ catch (e) {
957
+ console.log(` [warn] Global CLI update skipped: ${e.message}`);
958
+ console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}@latest`);
959
+ }
960
+ }
923
961
  try {
924
962
  const result = await execP(npmCommand, npmCommandArgs, {
925
963
  timeout: 300000,
@@ -989,12 +1027,27 @@ async function resolveScope() {
989
1027
  async function resolveProfile() {
990
1028
  const profile = defaultProfile();
991
1029
  // Single interactive prompt: the Combo preset implies all three modes.
1030
+ // Numbered menu, same pattern as the scope prompt — no typing preset names.
992
1031
  // Only for a real user at a terminal, only when no default flags were
993
1032
  // given, and never for --apply-update runs.
994
1033
  if (tty() && !profileFlagsGiven && !applyUpdate && (install || reinstall)) {
995
- const comboAnswer = (await ask(' Default Combo preset on session start? [off/medium/balanced/max] (off): ')).trim().toLowerCase();
996
- if (comboAnswer && COMBO_DEFAULTS.has(comboAnswer))
997
- profile.comboDefault = comboAnswer;
1034
+ console.log(' Session-start defaults — Combo preset:');
1035
+ console.log(' 1) off');
1036
+ console.log(' 2) medium (caveman=lite, rtk=on, ponytail=lite)');
1037
+ console.log(' 3) balanced (caveman=full, rtk=on, ponytail=full)');
1038
+ console.log(' 4) max (caveman=ultra, rtk=on, ponytail=ultra)');
1039
+ for (;;) {
1040
+ const answer = (await ask(' Choose [1-4] (default 1): ')).trim();
1041
+ if (!answer) {
1042
+ profile.comboDefault = 'off';
1043
+ break;
1044
+ }
1045
+ if (COMBO_MENU[answer]) {
1046
+ profile.comboDefault = COMBO_MENU[answer];
1047
+ break;
1048
+ }
1049
+ console.log(` [fail] Invalid choice: ${answer}. Choose 1, 2, 3, or 4.`);
1050
+ }
998
1051
  }
999
1052
  // Explicit flags always win over the Combo preset.
1000
1053
  if (comboDefaultFlag !== undefined)
@@ -1116,7 +1169,10 @@ async function main() {
1116
1169
  const userDir = OMP_AGENT_DIR;
1117
1170
  const userExtDir = path.join(userDir, 'extensions');
1118
1171
  const projectExtDir = path.join(process.cwd(), '.omp', 'extensions');
1119
- const options = { dryRun, verbose, yes, scope, reinstall };
1172
+ // apply-update is `tersio update`'s payload run: treat it like reinstall so
1173
+ // the add-ons refresh too — Ponytail package via npm, self plugin, RTK
1174
+ // binary (always re-downloaded), and the Caveman rule (always re-fetched).
1175
+ const options = { dryRun, verbose, yes, scope, reinstall: reinstall || applyUpdate };
1120
1176
  // Check prerequisites
1121
1177
  console.log('\nPrerequisites:');
1122
1178
  try {