@krtclcdy/tersio 2.18.0 → 2.20.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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/cli/common.js +21 -3
  3. package/cli/currency.js +102 -0
  4. package/cli/dashboard.js +54 -2
  5. package/cli/doctor.js +9 -7
  6. package/cli/install.js +35 -101
  7. package/cli/profile.js +92 -0
  8. package/cli/reset.js +4 -2
  9. package/cli/settings.js +122 -0
  10. package/cli/usage.js +38 -24
  11. package/dashboard/app.js +297 -70
  12. package/dashboard/styles.css +58 -1
  13. package/dashboard/template.html +75 -3
  14. package/extensions/ai-addons-updater/index.ts +377 -0
  15. package/extensions/caveman-session/index.ts +161 -0
  16. package/extensions/combo-toggle/index.ts +217 -0
  17. package/extensions/lib/utils.ts +162 -0
  18. package/extensions/rtk-session/index.ts +164 -0
  19. package/extensions/shared/carbon.ts +123 -0
  20. package/extensions/shared/mode-reinforcement.ts +29 -0
  21. package/extensions/shared/plugin-settings.ts +57 -0
  22. package/extensions/shared/pricing.ts +168 -0
  23. package/extensions/shared/rtk-gain.ts +105 -0
  24. package/extensions/shared/session-state.ts +201 -0
  25. package/extensions/shared/types.ts +85 -0
  26. package/extensions/shared/usage-ledger.ts +406 -0
  27. package/extensions/shared/usage-store.ts +325 -0
  28. package/extensions/tersio-commands/index.ts +110 -0
  29. package/package.json +36 -24
  30. package/tersio.js +8 -1
  31. package/extensions/ai-addons-updater/index.js +0 -331
  32. package/extensions/caveman-session/index.js +0 -152
  33. package/extensions/combo-toggle/index.js +0 -165
  34. package/extensions/lib/utils.js +0 -154
  35. package/extensions/rtk-session/index.js +0 -148
  36. package/extensions/shared/carbon.js +0 -88
  37. package/extensions/shared/mode-reinforcement.js +0 -26
  38. package/extensions/shared/plugin-settings.js +0 -51
  39. package/extensions/shared/pricing.js +0 -163
  40. package/extensions/shared/rtk-gain.js +0 -82
  41. package/extensions/shared/session-state.js +0 -177
  42. package/extensions/shared/types.js +0 -3
  43. package/extensions/shared/usage-ledger.js +0 -333
  44. package/extensions/tersio-commands/index.js +0 -102
package/README.md CHANGED
@@ -105,12 +105,12 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn c
105
105
  | `tersio reinstall` | Fresh install, preserving the Ponytail package |
106
106
  | `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
107
107
  | `tersio usage` | Ledger-backed usage + savings report |
108
- | `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) |
109
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 |
110
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) |
111
111
  | `tersio version` | Print version |
112
112
 
113
- 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.
114
114
 
115
115
  ## ⌨️ Commands reference
116
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');
@@ -141,6 +147,13 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
141
147
  const line = ` - ${normalizedPath}`;
142
148
  const raw = await readTextIfExists(configPath);
143
149
  let lines = (raw || '').split('\n');
150
+ // Drop legacy compiled twins (same dir/name, .js): previous installs
151
+ // registered them, and OMP must never load both copies. Match on the
152
+ // trailing path so absolute and relative entries are both caught.
153
+ if (normalizedPath.endsWith('.ts')) {
154
+ const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
155
+ lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
156
+ }
144
157
  if (lines.some((l) => l.includes(normalizedPath))) {
145
158
  debug(`${label} already in config.yml`);
146
159
  return false;
@@ -170,7 +183,12 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
170
183
  const normalizedAfterPath = afterPath.replace(/\\/g, '/');
171
184
  const line = ` - ${normalizedPath}`;
172
185
  const raw = await readTextIfExists(configPath);
173
- const lines = (raw || '').split('\n');
186
+ let lines = (raw || '').split('\n');
187
+ // Same legacy-twin drop as ensureExtensionInConfig above.
188
+ if (normalizedPath.endsWith('.ts')) {
189
+ const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
190
+ lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
191
+ }
174
192
  const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
175
193
  const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
176
194
  if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
@@ -267,4 +285,4 @@ function relTime(ageMs) {
267
285
  const weeks = Math.floor(days / 7);
268
286
  return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
269
287
  }
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, };
288
+ 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';
@@ -18,13 +19,13 @@ async function runDoctor() {
18
19
  // Ponytail
19
20
  const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
20
21
  const ponytailExt = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
21
- const cavemanIndex = path.join(extDir, 'caveman-session', 'index.js');
22
+ const cavemanIndex = path.join(extDir, 'caveman-session', 'index.ts');
22
23
  const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
23
- const rtkIndex = path.join(extDir, 'rtk-session', 'index.js');
24
- const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.js');
25
- const comboIndex = path.join(extDir, 'combo-toggle', 'index.js');
26
- const tersioIndex = path.join(extDir, 'tersio-commands', 'index.js');
27
- const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.js');
24
+ const rtkIndex = path.join(extDir, 'rtk-session', 'index.ts');
25
+ const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.ts');
26
+ const comboIndex = path.join(extDir, 'combo-toggle', 'index.ts');
27
+ const tersioIndex = path.join(extDir, 'tersio-commands', 'index.ts');
28
+ const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.ts');
28
29
  const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
29
30
  // Independent probes start concurrently; sections report in fixed order as
30
31
  // their data settles. Every probe resolves instead of rejecting.
@@ -32,7 +33,7 @@ async function runDoctor() {
32
33
  ompVersion: execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
33
34
  agentEntries: fs.readdir(agentDir).catch(() => null),
34
35
  extEntries: fs.readdir(extDir).catch(() => null),
35
- sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.js')),
36
+ sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.ts')),
36
37
  configText: readTextIfExists(configPath),
37
38
  ponytailPkgText: readTextIfExists(ponytailPkg),
38
39
  ponytailExtText: readTextIfExists(ponytailExt),
@@ -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,32 +3,34 @@ 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
- const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.js');
20
- const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.js');
21
- const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.js');
22
- const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.js');
23
- const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.js');
24
- const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.js');
25
- const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.js');
26
- const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.js');
27
- const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.js');
28
- const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.js');
29
- const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.js');
30
- const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.js');
31
- const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.js');
21
+ const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.ts');
22
+ const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.ts');
23
+ const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.ts');
24
+ const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.ts');
25
+ const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.ts');
26
+ const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.ts');
27
+ const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts');
28
+ const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.ts');
29
+ const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.ts');
30
+ const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts');
31
+ const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
32
+ const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
33
+ const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
32
34
  const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
33
35
  const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
34
36
  async function stepPonytail(pluginsDir, userDir, options) {
@@ -373,27 +375,27 @@ async function stepSharedSessionState(extDir, options) {
373
375
  if (!options.quiet)
374
376
  console.log(' Shared files — sync session bridge');
375
377
  await copySources(extDir, [
376
- [SHARED_SESSION_STATE, path.join('shared', 'session-state.js')],
377
- [SHARED_TYPES, path.join('shared', 'types.js')],
378
- [LIB_UTILS, path.join('lib', 'utils.js')],
379
- [SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.js')],
380
- [SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.js')],
381
- [SHARED_PRICING, path.join('shared', 'pricing.js')],
382
- [SHARED_CARBON, path.join('shared', 'carbon.js')],
378
+ [SHARED_SESSION_STATE, path.join('shared', 'session-state.ts')],
379
+ [SHARED_TYPES, path.join('shared', 'types.ts')],
380
+ [LIB_UTILS, path.join('lib', 'utils.ts')],
381
+ [SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.ts')],
382
+ [SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.ts')],
383
+ [SHARED_PRICING, path.join('shared', 'pricing.ts')],
384
+ [SHARED_CARBON, path.join('shared', 'carbon.ts')],
383
385
  ], 'shared/session-state.js', options);
384
386
  }
385
387
  async function stepModeReinforcement(extDir, ponytailExtPath, options) {
386
388
  if (!options.quiet)
387
389
  console.log(' Session helpers — sync shared files');
388
- const dest = path.join(extDir, 'shared', 'mode-reinforcement.js');
389
- if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.js')]], 'shared/mode-reinforcement.js', options))
390
+ const dest = path.join(extDir, 'shared', 'mode-reinforcement.ts');
391
+ if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.ts')]], 'shared/mode-reinforcement.ts', options))
390
392
  return;
391
393
  await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
392
394
  }
393
395
  async function stepRtkSession(extDir, options) {
394
396
  if (!options.quiet)
395
397
  console.log(' RTK session — install session mode');
396
- await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.js')]], 'rtk-session/index.js', options);
398
+ await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.ts')]], 'rtk-session/index.ts', options);
397
399
  }
398
400
  // One rule fetch serves the install; dry runs stay offline
399
401
  // and fall back to the bundled rule to preview its destination.
@@ -425,65 +427,26 @@ async function stepCaveman(extDir, rule, options) {
425
427
  else {
426
428
  await writeIfChanged(ruleDest, rule, options);
427
429
  }
428
- await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.js')]], 'caveman-session/index.js', options);
430
+ await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.ts')]], 'caveman-session/index.ts', options);
429
431
  }
430
432
  async function stepTersioCommands(extDir, options) {
431
433
  if (!options.quiet)
432
434
  console.log(' Tersio commands — install /tersio root command');
433
- await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.js')]], 'tersio-commands/index.js', options);
435
+ await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.ts')]], 'tersio-commands/index.ts', options);
434
436
  }
435
437
  async function stepUpdater(extDir, options) {
436
- await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.js')]], 'ai-addons-updater/index.js', options);
438
+ await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.ts')]], 'ai-addons-updater/index.ts', options);
437
439
  }
438
440
  async function stepCombo(extDir, options) {
439
441
  if (!options.quiet)
440
442
  console.log(' Combo — install preset switch');
441
- const dest = path.join(extDir, 'combo-toggle', 'index.js');
442
- if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.js')]], 'combo-toggle/index.js', options))
443
+ const dest = path.join(extDir, 'combo-toggle', 'index.ts');
444
+ if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.ts')]], 'combo-toggle/index.ts', options))
443
445
  return;
444
446
  // Auto-register combo in config.yml
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 };