@krtclcdy/tersio 2.17.0 → 2.19.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
- ![Banner](/TersioBanner.webp)
1
+ ![Banner](/assets/Banner.webp)
2
2
 
3
3
  # ✂️ Tersio — Token-saving OMP Add-ons
4
4
 
@@ -8,7 +8,7 @@
8
8
  [![Build](https://shieldcn.dev/github/ci/KurutoDenzeru/tersio.svg?variant=branded&size=xs&logo=githubactions&label=Build)](https://github.com/KurutoDenzeru/tersio/actions)
9
9
  [![MIT](https://shieldcn.dev/badge/license-MIT-2563eb.svg?variant=branded&size=xs&logo=opensourceinitiative)](./LICENSE)
10
10
 
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.
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
12
 
13
13
  ## ⚡ Getting Started
14
14
 
@@ -80,6 +80,22 @@ Measured savings against the same workload without the modes. Token counts are r
80
80
 
81
81
  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).
82
82
 
83
+ ## 📈 Gain Dashboard
84
+
85
+ `tersio gain --open` serves a local dashboard (127.0.0.1 only) that charts your own savings from `~/.omp/plugins/tersio-usage.jsonl`. Three views:
86
+
87
+ **Live feed and savings.** Token throughput, cost, savings bento, and recent activity — the top of the dashboard.
88
+
89
+ ![Gain dashboard: live token feed, cost, savings, and activity](/assets/GainHero.webp)
90
+
91
+ **Top models and recent requests.** Per-model cost breakdown plus the last requests with elapsed time and token speed.
92
+
93
+ ![Gain dashboard: top models with per-model cost, recent requests with elapsed time and token speed](/assets/GainModels.webp)
94
+
95
+ **Command tools.** Token savings, average rate, and timing per shell tool.
96
+
97
+ ![Gain dashboard: command tools with token savings, average rate, and timing](/assets/GainTools.webp)
98
+
83
99
  ## 🖥️ CLI
84
100
 
85
101
  | Command | Purpose |
@@ -89,12 +105,12 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn c
89
105
  | `tersio reinstall` | Fresh install, preserving the Ponytail package |
90
106
  | `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
91
107
  | `tersio usage` | Ledger-backed usage + savings report |
92
- | `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`; serves localhost only) |
108
+ | `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`, `--currency <code>`; serves localhost only) |
93
109
  | `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 |
94
110
  | `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) |
95
111
  | `tersio version` | Print version |
96
112
 
97
- Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`. Legacy `--doctor` / `--uninstall` forms still work.
113
+ Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`, `--currency <code>` (usage|gain display currency; flag wins, then the `tersio settings` default, then USD). Legacy `--doctor` / `--uninstall` forms still work.
98
114
 
99
115
  ## ⌨️ Commands reference
100
116
 
package/cli/common.js CHANGED
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { createRequire } from 'node:module';
7
7
  import { readTextIfExists } from '../extensions/lib/utils.js';
8
+ import { parseCurrencyFlag, readStoredCurrency } from './currency.js';
8
9
  const IS_WINDOWS = process.platform === 'win32';
9
10
  const RTK_BINARY_NAME = IS_WINDOWS ? 'rtk.exe' : 'rtk';
10
11
  const HOME = process.env.HOME || process.env.USERPROFILE || '';
@@ -46,7 +47,7 @@ function flagValue(name) {
46
47
  return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
47
48
  }
48
49
  // --- CLI flags ---
49
- const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, reset: true, version: true, help: true };
50
+ const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, reset: true, settings: true, version: true, help: true };
50
51
  const args = process.argv.slice(2);
51
52
  const commandArg = args.find((arg) => !arg.startsWith('-'));
52
53
  const command = commandArg?.toLowerCase() || null;
@@ -64,9 +65,14 @@ const doctor = command === 'doctor' || args.includes('--doctor');
64
65
  const usage = command === 'usage' || args.includes('--usage');
65
66
  const gain = command === 'gain';
66
67
  const reset = command === 'reset';
68
+ const settings = command === 'settings';
67
69
  const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
68
70
  const dashboardOpen = args.includes('--open');
69
71
  const dashboardExport = flagValue('--export') ?? null;
72
+ // Display currency for usage/gain: --currency flag wins, then the stored
73
+ // plugin default (tersio settings), then USD.
74
+ const currency = parseCurrencyFlag(flagValue('--currency')) ?? readStoredCurrency();
75
+ const currencyGiven = flagValue('--currency') !== undefined;
70
76
  const uninstall = command === 'uninstall' || args.includes('--uninstall');
71
77
  const removePonytail = args.includes('--remove-ponytail');
72
78
  const keepPonytail = args.includes('--keep-ponytail');
@@ -267,4 +273,4 @@ function relTime(ageMs) {
267
273
  const weeks = Math.floor(days / 7);
268
274
  return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
269
275
  }
270
- 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, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
276
+ 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, settings, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
@@ -0,0 +1,102 @@
1
+ // cli/currency.ts — display-currency helpers (USD-base, offline snapshot rates).
2
+ // Same 10 currencies, symbols, and snapshot rates as the gain dashboard's
3
+ // converter (dashboard/app.js); the dashboard replaces rates with live
4
+ // frankfurter figures when reachable, the CLI always uses the snapshot.
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ const CURRENCY_CODES = [
9
+ 'USD',
10
+ 'PHP',
11
+ 'EUR',
12
+ 'GBP',
13
+ 'JPY',
14
+ 'KRW',
15
+ 'SGD',
16
+ 'AUD',
17
+ 'CAD',
18
+ 'INR',
19
+ ];
20
+ const DEFAULT_CURRENCY = 'USD';
21
+ function isCurrencyCode(value) {
22
+ return CURRENCY_CODES.includes(value);
23
+ }
24
+ // --currency flag parsing: undefined when absent, exits on invalid input.
25
+ function parseCurrencyFlag(raw) {
26
+ if (raw === undefined)
27
+ return undefined;
28
+ const v = raw.trim().toUpperCase();
29
+ if (!isCurrencyCode(v)) {
30
+ console.error(`[fail] Invalid --currency: ${raw}. Valid: ${CURRENCY_CODES.join(', ')}`);
31
+ process.exit(1);
32
+ }
33
+ return v;
34
+ }
35
+ // Stored default from the omp plugin lock file (written by tersio settings);
36
+ // USD when missing, corrupt, or holding an unknown code. Sync so flag
37
+ // parsing in cli/common.ts can fall back to it at startup.
38
+ function readStoredCurrency() {
39
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
40
+ const lock = path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json');
41
+ if (!existsSync(lock))
42
+ return DEFAULT_CURRENCY;
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(lock, 'utf8'));
45
+ if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
46
+ return DEFAULT_CURRENCY;
47
+ const entry = parsed.settings?.['@krtclcdy/tersio'];
48
+ if (!entry || typeof entry !== 'object')
49
+ return DEFAULT_CURRENCY;
50
+ const raw = entry.currency;
51
+ return typeof raw === 'string' && isCurrencyCode(raw.trim().toUpperCase())
52
+ ? raw.trim().toUpperCase()
53
+ : DEFAULT_CURRENCY;
54
+ }
55
+ catch {
56
+ return DEFAULT_CURRENCY;
57
+ }
58
+ }
59
+ const CURRENCY_META = {
60
+ USD: { symbol: '$', decimals: 2 },
61
+ PHP: { symbol: '₱', decimals: 2 },
62
+ EUR: { symbol: '€', decimals: 2 },
63
+ GBP: { symbol: '£', decimals: 2 },
64
+ JPY: { symbol: '¥', decimals: 0 },
65
+ KRW: { symbol: '₩', decimals: 0 },
66
+ SGD: { symbol: 'S$', decimals: 2 },
67
+ AUD: { symbol: 'A$', decimals: 2 },
68
+ CAD: { symbol: 'C$', decimals: 2 },
69
+ INR: { symbol: '₹', decimals: 2 },
70
+ };
71
+ const FX_SNAPSHOT_RATES = {
72
+ USD: 1,
73
+ PHP: 58.7,
74
+ EUR: 0.92,
75
+ GBP: 0.79,
76
+ JPY: 149.8,
77
+ KRW: 1385,
78
+ SGD: 1.34,
79
+ AUD: 1.52,
80
+ CAD: 1.37,
81
+ INR: 88.2,
82
+ };
83
+ function decimalsFor(amount, base) {
84
+ const a = Math.abs(amount);
85
+ if (a >= 0.1 || a === 0)
86
+ return base;
87
+ if (a >= 0.001)
88
+ return Math.max(base, 4);
89
+ if (a >= 0.00001)
90
+ return Math.max(base, 6);
91
+ return Math.max(base, 8);
92
+ }
93
+ function convertUsd(usd, currency) {
94
+ return usd * FX_SNAPSHOT_RATES[currency];
95
+ }
96
+ function formatCurrency(usd, currency) {
97
+ const meta = CURRENCY_META[currency];
98
+ const amount = convertUsd(usd, currency);
99
+ const d = decimalsFor(amount, meta.decimals);
100
+ return meta.symbol + amount.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
101
+ }
102
+ export { CURRENCY_CODES, DEFAULT_CURRENCY, convertUsd, formatCurrency, isCurrencyCode, parseCurrencyFlag, readStoredCurrency, };
package/cli/dashboard.js CHANGED
@@ -10,6 +10,8 @@ import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
12
12
  import { summarizeUsage } from './usage.js';
13
+ import { isCurrencyCode } from './currency.js';
14
+ import { storedProfile, writePluginSettings } from './profile.js';
13
15
  import { withInteractiveSpinner } from './interactive.js';
14
16
  const DASH_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard');
15
17
  const TEMPLATE = path.join(DASH_DIR, 'template.html');
@@ -26,6 +28,29 @@ async function faviconDataUri() {
26
28
  function dataJson() {
27
29
  return JSON.stringify(summarizeUsage(readUsage()));
28
30
  }
31
+ // Persist the dashboard's picker choice so close → reopen keeps it: each
32
+ // `tersio gain` run serves a fresh ephemeral port (a new origin), so the
33
+ // browser's localStorage alone cannot survive a restart. The stored default
34
+ // feeds data.json and `tersio usage` on the next run.
35
+ async function saveDashboardCurrency(raw) {
36
+ if (typeof raw !== 'string')
37
+ return null;
38
+ const code = raw.trim().toUpperCase();
39
+ if (!isCurrencyCode(code))
40
+ return null;
41
+ const profile = await storedProfile();
42
+ profile.currency = code;
43
+ await writePluginSettings(profile, {});
44
+ return code;
45
+ }
46
+ function readBody(req) {
47
+ return new Promise((resolve) => {
48
+ let body = '';
49
+ req.on('data', (chunk) => { body += String(chunk); });
50
+ req.on('end', () => { resolve(body); });
51
+ req.on('error', () => { resolve(''); });
52
+ });
53
+ }
29
54
  function openBrowser(url) {
30
55
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
31
56
  spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
@@ -38,10 +63,18 @@ async function runDashboard(options) {
38
63
  // Replacer functions throughout: session data routinely contains `$'`
39
64
  // sequences (shell quoting in tool details), which String.replace would
40
65
  // expand as match-suffix patterns and corrupt the file.
66
+ // RTK-metered command lines routinely contain literal `</script>` (Vue
67
+ // SFC probes), which would close the inlined <script> early and dump the
68
+ // rest of the JSON as page text. Escape it; JSON.parse never sees the
69
+ // backslash form inside a string literal, the browser decodes it first.
41
70
  const inline = template
42
71
  .replace('<link rel="stylesheet" href="styles.css">', () => `<style>\n${css}</style>`)
43
72
  .replace('<script src="app.js" defer></script>', () => `<script>\n${js}</script>`)
44
- .replace("fetch('data.json')", () => `Promise.resolve({ json: function () { return ${dataJson()}; } })`)
73
+ .replace("fetch('data.json')", () => `Promise.resolve({ json: function () { return ${dataJson().replace(/<\/(script)/gi, '<\\/$1')}; } })`)
74
+ // Exported file runs on file:// where load() early-returns before the
75
+ // stub above ever runs, so nothing ever renders. Drop that guard in
76
+ // the export only; template.html keeps it to avoid 404 polling loops.
77
+ .replace("if (window.location.protocol === 'file:') return;", () => `if (false) return;`)
45
78
  .replace('href="brand.webp"', () => `href="${icon}"`)
46
79
  .replace('src="brand.webp"', () => `src="${icon}"`);
47
80
  await fs.writeFile(options.exportFile, inline, 'utf8');
@@ -57,6 +90,25 @@ async function runDashboard(options) {
57
90
  res.end(JSON.stringify({ ok: true, rows }));
58
91
  return;
59
92
  }
93
+ if (req.url === '/currency' && req.method === 'POST') {
94
+ let code = null;
95
+ try {
96
+ code = JSON.parse(await readBody(req)).currency ?? null;
97
+ }
98
+ catch {
99
+ code = null;
100
+ }
101
+ const saved = await saveDashboardCurrency(code);
102
+ if (saved === null) {
103
+ res.writeHead(400, { 'Content-Type': 'application/json' });
104
+ res.end(JSON.stringify({ ok: false, error: 'unknown currency' }));
105
+ }
106
+ else {
107
+ res.writeHead(200, { 'Content-Type': 'application/json' });
108
+ res.end(JSON.stringify({ ok: true, currency: saved }));
109
+ }
110
+ return;
111
+ }
60
112
  if (req.url === '/data.json') {
61
113
  res.writeHead(200, { 'Content-Type': 'application/json' });
62
114
  res.end(dataJson());
@@ -100,4 +152,4 @@ async function runDashboard(options) {
100
152
  openBrowser(url);
101
153
  });
102
154
  }
103
- export { runDashboard };
155
+ export { runDashboard, saveDashboardCurrency };
package/cli/doctor.js CHANGED
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { BUN_BIN_DIR, 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
6
  import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
7
+ import { usageDbPath } from '../extensions/shared/usage-store.js';
7
8
  import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
8
9
  import { checkForUpdate } from './update.js';
9
10
  import { readTextIfExists } from '../extensions/lib/utils.js';
@@ -136,6 +137,7 @@ async function runDoctor() {
136
137
  check('Usage ledger', true, usageRows.length ? `${usageRows.length} rows` : 'empty — no records yet');
137
138
  console.log(` Usage ledger (tersio-owned, tersio reset clears): ${ledgerPath()} · ${usageRows.length} rows`);
138
139
  console.log(` Session transcripts (host-owned, never touched): ${sessionsDir()}`);
140
+ console.log(` Usage DB (tersio-owned, tersio reset clears): ${usageDbPath()}`);
139
141
  const rtkDb = rtkDbPath();
140
142
  const rtk = readRtkGain();
141
143
  console.log(` RTK history (rtk-owned, never touched): ${rtkDb}${rtk.commands ? ` · ${rtk.commands} commands` : ''}`);
package/cli/install.js CHANGED
@@ -3,17 +3,19 @@ import { promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { BUN_BIN_DIR, CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, PONYTAIL_DEFAULTS, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
6
+ import { BUN_BIN_DIR, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
7
7
  import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
8
8
  import { printWelcome } from './banner.js';
9
9
  import { checkForUpdate, runLatestUpdate } from './update.js';
10
10
  import { runUninstall } from './uninstall.js';
11
11
  import { runDoctor } from './doctor.js';
12
12
  import { runReset } from './reset.js';
13
+ import { runSettings } from './settings.js';
13
14
  import { runUsage } from './usage.js';
14
15
  import { runDashboard } from './dashboard.js';
15
16
  import { wireRtkOmp } from './rtk-wiring.js';
16
17
  import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
18
+ import { storedProfile, writePluginSettings } from './profile.js';
17
19
  // Paths to extension source files (relative to this script)
18
20
  const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
19
21
  const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.js');
@@ -445,45 +447,6 @@ async function stepCombo(extDir, options) {
445
447
  const configPath = path.join(path.dirname(extDir), 'config.yml');
446
448
  await ensureExtensionInConfig(configPath, dest, 'combo', options);
447
449
  }
448
- function defaultProfile() {
449
- return {
450
- comboDefault: 'off',
451
- cavemanDefault: 'off',
452
- rtkDefault: false,
453
- ponytailDefault: 'off',
454
- };
455
- }
456
- async function storedProfile() {
457
- const base = defaultProfile();
458
- const raw = await readTextIfExists(path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json'));
459
- if (!raw)
460
- return base;
461
- let stored;
462
- try {
463
- const parsed = JSON.parse(raw);
464
- if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
465
- return base;
466
- const settings = parsed.settings;
467
- if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
468
- return base;
469
- const entry = settings[PACKAGE_NAME];
470
- if (!entry || typeof entry !== 'object')
471
- return base;
472
- stored = entry;
473
- }
474
- catch {
475
- return base;
476
- }
477
- if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
478
- base.comboDefault = stored.comboDefault;
479
- if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
480
- base.cavemanDefault = stored.cavemanDefault;
481
- if (typeof stored.rtkDefault === 'boolean')
482
- base.rtkDefault = stored.rtkDefault;
483
- if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
484
- base.ponytailDefault = stored.ponytailDefault;
485
- return base;
486
- }
487
450
  async function resolveProfile(forceReinstall = false, opts = {}) {
488
451
  // Seed from the lock file so flag-less update/reinstall runs keep the
489
452
  // user's configured defaults instead of resetting them to off.
@@ -523,39 +486,6 @@ async function resolveProfile(forceReinstall = false, opts = {}) {
523
486
  console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
524
487
  return profile;
525
488
  }
526
- // Persist the profile as omp plugin settings so `omp plugin config get`
527
- // reflects the choice and the extensions pick it up on session start.
528
- async function writePluginSettings(profile, options) {
529
- const pluginsDir = OMP_PLUGINS_DIR;
530
- const lockPath = path.join(pluginsDir, 'omp-plugins.lock.json');
531
- let config = {};
532
- const existing = await readTextIfExists(lockPath);
533
- if (existing) {
534
- try {
535
- config = JSON.parse(existing);
536
- }
537
- catch {
538
- config = {};
539
- }
540
- }
541
- config.plugins = config.plugins || {};
542
- config.settings = config.settings || {};
543
- config.settings[PACKAGE_NAME] = {
544
- ...(config.settings[PACKAGE_NAME] || {}),
545
- comboDefault: profile.comboDefault,
546
- cavemanDefault: profile.cavemanDefault,
547
- rtkDefault: profile.rtkDefault,
548
- ponytailDefault: profile.ponytailDefault,
549
- };
550
- if (options.dryRun) {
551
- if (verbose && !options.quiet)
552
- console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
553
- return;
554
- }
555
- await fs.mkdir(pluginsDir, { recursive: true });
556
- await fs.writeFile(lockPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
557
- console.log(` [write] Plugin settings in ${lockPath}`);
558
- }
559
489
  let updatePromptDone = false;
560
490
  // Bare `tersio` at a terminal is a command picker, not an install run:
561
491
  // update offer first (when pending), then a Clack menu over every command.
@@ -585,6 +515,7 @@ async function runCommandMenu() {
585
515
  { value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
586
516
  { value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
587
517
  { value: 'reset', label: 'Reset statistics', hint: 'clear statistics; transcripts and RTK history stay' },
518
+ { value: 'settings', label: 'Settings', hint: 'defaults: combo, caveman, rtk, ponytail, currency' },
588
519
  { value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
589
520
  ], 'install');
590
521
  if (choice.status !== 'selected') {
@@ -636,6 +567,9 @@ async function runCommandMenu() {
636
567
  await runReset();
637
568
  closeRL();
638
569
  break;
570
+ case 'settings':
571
+ await runSettings();
572
+ break;
639
573
  case 'uninstall':
640
574
  await runUninstall();
641
575
  closeRL();
package/cli/profile.js ADDED
@@ -0,0 +1,92 @@
1
+ // cli/profile.ts — session-start defaults profile (combo/caveman/rtk/ponytail)
2
+ // plus the display-currency default for usage/gain reports.
3
+ // Single source for reading/writing the omp plugin settings lock entry.
4
+ // Extracted from cli/install.ts so both install and settings share it.
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
8
+ import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
9
+ import { readTextIfExists } from '../extensions/lib/utils.js';
10
+ function defaultProfile() {
11
+ return {
12
+ comboDefault: 'off',
13
+ cavemanDefault: 'off',
14
+ rtkDefault: false,
15
+ ponytailDefault: 'off',
16
+ currency: DEFAULT_CURRENCY,
17
+ };
18
+ }
19
+ // Stored profile from the live lock file: update/reinstall runs without flags
20
+ // or prompts must preserve the user's choice, never reset it to off.
21
+ async function storedProfile() {
22
+ const base = defaultProfile();
23
+ const raw = await readTextIfExists(path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json'));
24
+ if (!raw)
25
+ return base;
26
+ let stored;
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
30
+ return base;
31
+ const settings = parsed.settings;
32
+ if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
33
+ return base;
34
+ const entry = settings[PACKAGE_NAME];
35
+ if (!entry || typeof entry !== 'object')
36
+ return base;
37
+ stored = entry;
38
+ }
39
+ catch {
40
+ return base;
41
+ }
42
+ if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
43
+ base.comboDefault = stored.comboDefault;
44
+ if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
45
+ base.cavemanDefault = stored.cavemanDefault;
46
+ if (typeof stored.rtkDefault === 'boolean')
47
+ base.rtkDefault = stored.rtkDefault;
48
+ if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
49
+ base.ponytailDefault = stored.ponytailDefault;
50
+ if (typeof stored.currency === 'string' && isCurrencyCode(stored.currency.trim().toUpperCase())) {
51
+ base.currency = stored.currency.trim().toUpperCase();
52
+ }
53
+ return base;
54
+ }
55
+ // Persist the profile as omp plugin settings so `omp plugin config get`
56
+ // reflects the choice and the extensions pick it up on session start.
57
+ async function writePluginSettings(profile, options) {
58
+ const pluginsDir = OMP_PLUGINS_DIR;
59
+ const lockPath = path.join(pluginsDir, 'omp-plugins.lock.json');
60
+ let config = {};
61
+ const existing = await readTextIfExists(lockPath);
62
+ if (existing) {
63
+ try {
64
+ config = JSON.parse(existing);
65
+ }
66
+ catch {
67
+ config = {};
68
+ }
69
+ }
70
+ config.plugins = config.plugins || {};
71
+ config.settings = config.settings || {};
72
+ config.settings[PACKAGE_NAME] = {
73
+ ...(config.settings[PACKAGE_NAME] || {}),
74
+ comboDefault: profile.comboDefault,
75
+ cavemanDefault: profile.cavemanDefault,
76
+ rtkDefault: profile.rtkDefault,
77
+ ponytailDefault: profile.ponytailDefault,
78
+ currency: profile.currency,
79
+ };
80
+ if (options.dryRun) {
81
+ if (verbose && !options.quiet)
82
+ console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
83
+ return;
84
+ }
85
+ await fs.mkdir(pluginsDir, { recursive: true });
86
+ await fs.writeFile(lockPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
87
+ console.log(` [write] Plugin settings in ${lockPath}`);
88
+ }
89
+ function formatProfile(profile) {
90
+ return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault}) · currency=${profile.currency}`;
91
+ }
92
+ export { defaultProfile, formatProfile, storedProfile, writePluginSettings };
package/cli/reset.js CHANGED
@@ -7,13 +7,14 @@ import { cancel as clackCancel, confirm as clackConfirm } from '@clack/prompts';
7
7
  import { dryRun, yes } from './common.js';
8
8
  import { ask, closeRL, tty } from './interactive.js';
9
9
  import { clearUsageLedger, importSessionTokens, ledgerPath, markReset, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
10
+ import { clearUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
10
11
  import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
11
12
  async function runReset() {
12
13
  console.log('\n=== Tersio Reset ===\n');
13
14
  const rows = readUsage().length;
14
15
  const sessions = importSessionTokens();
15
16
  const rtk = readRtkGain(1);
16
- console.log(`Will clear: ${rows} usage ledger rows · ${ledgerPath()}`);
17
+ console.log(`Will clear: ${rows} usage ledger rows · ${ledgerPath()} + usage.db ${usageDbPath()}`);
17
18
  console.log(`Will hide (view-level watermark): session-derived statistics (${sessions.messages} messages) · RTK metered rows (${rtk.commands})`);
18
19
  console.log(`Left intact on disk: sessions ${sessionsDir()}, rtk ${rtkDbPath()}`);
19
20
  if (dryRun) {
@@ -49,8 +50,9 @@ async function runReset() {
49
50
  }
50
51
  }
51
52
  const cleared = clearUsageLedger();
53
+ const dbCleared = clearUsageDb();
52
54
  const ts = markReset();
53
- console.log(`[ok] reset — removed ${cleared} usage rows; statistics view starts at ${new Date(ts).toISOString()}`);
55
+ console.log(`[ok] reset — removed ${cleared} usage rows${dbCleared ? ' + usage.db' : ''}; statistics view starts at ${new Date(ts).toISOString()}`);
54
56
  return true;
55
57
  }
56
58
  export { runReset };