@krtclcdy/tersio 2.18.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 +2 -2
- package/cli/common.js +8 -2
- package/cli/currency.js +102 -0
- package/cli/dashboard.js +54 -2
- package/cli/doctor.js +2 -0
- package/cli/install.js +7 -73
- package/cli/profile.js +92 -0
- package/cli/reset.js +4 -2
- package/cli/settings.js +122 -0
- package/cli/usage.js +38 -24
- package/dashboard/app.js +297 -70
- package/dashboard/styles.css +58 -1
- package/dashboard/template.html +75 -3
- package/extensions/shared/rtk-gain.js +3 -6
- package/extensions/shared/usage-ledger.js +124 -103
- package/extensions/shared/usage-store.js +294 -0
- package/package.json +18 -1
- package/tersio.js +8 -1
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
|
|
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');
|
|
@@ -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, };
|
package/cli/currency.js
ADDED
|
@@ -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,
|
|
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 };
|
package/cli/settings.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// cli/settings.ts — view/edit session-start defaults (combo/caveman/rtk/ponytail)
|
|
2
|
+
// plus the display-currency default for usage/gain reports.
|
|
3
|
+
// Non-interactive: `tersio settings --combo-default balanced` (plus per-mode
|
|
4
|
+
// override flags and --currency). Interactive: clack selects seeded from the
|
|
5
|
+
// stored profile.
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, } from './common.js';
|
|
8
|
+
import { CURRENCY_CODES } from './currency.js';
|
|
9
|
+
import { textTable } from './usage.js';
|
|
10
|
+
import { askInteractiveChoice, closeRL, tty } from './interactive.js';
|
|
11
|
+
import { formatProfile, storedProfile, writePluginSettings } from './profile.js';
|
|
12
|
+
function applyFlags(base) {
|
|
13
|
+
const next = { ...base };
|
|
14
|
+
if (comboDefaultFlag !== undefined)
|
|
15
|
+
next.comboDefault = comboDefaultFlag;
|
|
16
|
+
const preset = COMBO_PRESET_MODES[next.comboDefault] ?? COMBO_PRESET_MODES.off;
|
|
17
|
+
next.cavemanDefault = preset.caveman;
|
|
18
|
+
next.rtkDefault = preset.rtk;
|
|
19
|
+
next.ponytailDefault = preset.ponytail;
|
|
20
|
+
if (cavemanDefaultFlag !== undefined)
|
|
21
|
+
next.cavemanDefault = cavemanDefaultFlag;
|
|
22
|
+
if (rtkDefaultFlag !== undefined)
|
|
23
|
+
next.rtkDefault = rtkDefaultFlag === 'on';
|
|
24
|
+
if (ponytailDefaultFlag !== undefined)
|
|
25
|
+
next.ponytailDefault = ponytailDefaultFlag;
|
|
26
|
+
if (currencyGiven)
|
|
27
|
+
next.currency = currency;
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
async function askProfile(current) {
|
|
31
|
+
const combo = await askInteractiveChoice('Session-start defaults — Combo preset', [
|
|
32
|
+
{ value: 'off', label: 'off' },
|
|
33
|
+
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
34
|
+
{ value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
|
|
35
|
+
{ value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
36
|
+
], current.comboDefault);
|
|
37
|
+
if (combo.status !== 'selected')
|
|
38
|
+
return null;
|
|
39
|
+
const preset = COMBO_PRESET_MODES[combo.value] ?? COMBO_PRESET_MODES.off;
|
|
40
|
+
const next = {
|
|
41
|
+
comboDefault: combo.value,
|
|
42
|
+
cavemanDefault: preset.caveman,
|
|
43
|
+
rtkDefault: preset.rtk,
|
|
44
|
+
ponytailDefault: preset.ponytail,
|
|
45
|
+
currency: current.currency,
|
|
46
|
+
};
|
|
47
|
+
const caveman = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
|
|
48
|
+
value: v, label: v,
|
|
49
|
+
})), next.cavemanDefault);
|
|
50
|
+
if (caveman.status !== 'selected')
|
|
51
|
+
return null;
|
|
52
|
+
next.cavemanDefault = caveman.value;
|
|
53
|
+
const rtk = await askInteractiveChoice('RTK default', [
|
|
54
|
+
{ value: 'on', label: 'on' },
|
|
55
|
+
{ value: 'off', label: 'off' },
|
|
56
|
+
], next.rtkDefault ? 'on' : 'off');
|
|
57
|
+
if (rtk.status !== 'selected')
|
|
58
|
+
return null;
|
|
59
|
+
next.rtkDefault = rtk.value === 'on';
|
|
60
|
+
const ponytail = await askInteractiveChoice('Ponytail default', [...PONYTAIL_DEFAULTS].map((v) => ({
|
|
61
|
+
value: v, label: v,
|
|
62
|
+
})), next.ponytailDefault);
|
|
63
|
+
if (ponytail.status !== 'selected')
|
|
64
|
+
return null;
|
|
65
|
+
next.ponytailDefault = ponytail.value;
|
|
66
|
+
const cur = await askInteractiveChoice('Display currency (usage/gain reports)', CURRENCY_CODES.map((v) => ({
|
|
67
|
+
value: v, label: v,
|
|
68
|
+
})), next.currency);
|
|
69
|
+
if (cur.status !== 'selected')
|
|
70
|
+
return null;
|
|
71
|
+
next.currency = cur.value;
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
function printSettingsTable(current) {
|
|
75
|
+
console.log('\n=== Tersio Settings ===');
|
|
76
|
+
// Currency lives here no longer: the gain dashboard owns displaying and
|
|
77
|
+
// persisting it (its picker POSTs to /currency); settings can still set
|
|
78
|
+
// the stored default via --currency or the prompt below.
|
|
79
|
+
const rows = [
|
|
80
|
+
['combo', current.comboDefault, [...COMBO_DEFAULTS].join(' | ')],
|
|
81
|
+
['caveman', current.cavemanDefault, [...CAVEMAN_DEFAULTS].join(' | ')],
|
|
82
|
+
['rtk', current.rtkDefault ? 'on' : 'off', 'on | off'],
|
|
83
|
+
['ponytail', current.ponytailDefault, [...PONYTAIL_DEFAULTS].join(' | ')],
|
|
84
|
+
];
|
|
85
|
+
for (const l of textTable(['Setting', 'Current', 'Valid values'], rows, [false, false, false], 64)) {
|
|
86
|
+
console.log(l);
|
|
87
|
+
}
|
|
88
|
+
console.log(` Stored: ${PACKAGE_NAME} in ${path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json')}`);
|
|
89
|
+
}
|
|
90
|
+
async function runSettings() {
|
|
91
|
+
const current = await storedProfile();
|
|
92
|
+
printSettingsTable(current);
|
|
93
|
+
let next;
|
|
94
|
+
if (profileFlagsGiven || currencyGiven) {
|
|
95
|
+
next = applyFlags(current);
|
|
96
|
+
}
|
|
97
|
+
else if (tty()) {
|
|
98
|
+
next = await askProfile(current);
|
|
99
|
+
if (next === null) {
|
|
100
|
+
closeRL();
|
|
101
|
+
process.exit(130);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log('\n No flags given and no TTY — nothing to change.');
|
|
107
|
+
console.log(' Usage: tersio settings [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|wenyan] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra|review] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--dry-run]');
|
|
108
|
+
closeRL();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (dryRun) {
|
|
112
|
+
console.log(`\n [dry-run] would set defaults: ${formatProfile(next)}`);
|
|
113
|
+
closeRL();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await writePluginSettings(next, {});
|
|
117
|
+
console.log(` Defaults: ${formatProfile(next)}`);
|
|
118
|
+
console.log(' Applies to fresh sessions only — anything persisted with /combo, /caveman, or /rtk wins.');
|
|
119
|
+
console.log(' Currency applies to usage/gain reports; --currency overrides it per run.');
|
|
120
|
+
closeRL();
|
|
121
|
+
}
|
|
122
|
+
export { runSettings };
|