@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 +20 -4
- 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 +42 -16
- package/dashboard/app.js +435 -67
- package/dashboard/styles.css +83 -3
- package/dashboard/template.html +97 -17
- package/extensions/caveman-session/index.js +3 -4
- package/extensions/combo-toggle/index.js +3 -4
- package/extensions/rtk-session/index.js +3 -4
- package/extensions/shared/rtk-gain.js +13 -1
- package/extensions/shared/session-state.js +15 -0
- package/extensions/shared/usage-ledger.js +155 -83
- package/extensions/shared/usage-store.js +294 -0
- package/package.json +18 -1
- package/tersio.js +8 -1
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 };
|
package/cli/usage.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import { co2GramsFor, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
1
|
+
import { co2GramsFor, displayModelId, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
2
|
import { readRtkGain } from '../extensions/shared/rtk-gain.js';
|
|
3
|
+
import { readUsageDb, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
4
4
|
import { withInteractiveSpinner } from './interactive.js';
|
|
5
|
-
import { PACKAGE_VERSION } from './common.js';
|
|
5
|
+
import { PACKAGE_VERSION, currency } from './common.js';
|
|
6
|
+
import { formatCurrency } from './currency.js';
|
|
7
|
+
// RTK's history is machine-wide; rows group by command alone. 2000 groups
|
|
8
|
+
// covers a long history while still bounding /data.json.
|
|
9
|
+
const RTK_COMMAND_ROWS = 2000;
|
|
6
10
|
export function summarizeUsage(rows) {
|
|
7
11
|
refreshPricesIfStale();
|
|
8
12
|
const byKind = {};
|
|
@@ -12,7 +16,24 @@ export function summarizeUsage(rows) {
|
|
|
12
16
|
detailCounts[r.detail] = (detailCounts[r.detail] ?? 0) + 1;
|
|
13
17
|
}
|
|
14
18
|
const byDetail = Object.entries(detailCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
15
|
-
|
|
19
|
+
// usage.db is a cache of the same per-message rows: sync best-effort, read
|
|
20
|
+
// stored, fall back to the live parse when the store is missing or stale.
|
|
21
|
+
let synced = false;
|
|
22
|
+
try {
|
|
23
|
+
synced = syncUsageDb();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
synced = false;
|
|
27
|
+
}
|
|
28
|
+
let stored = null;
|
|
29
|
+
try {
|
|
30
|
+
stored = readUsageDb()?.tokens ?? null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
stored = null;
|
|
34
|
+
}
|
|
35
|
+
const session = stored ?? importSessionTokens();
|
|
36
|
+
const source = stored ? (synced ? 'stored' : 'stored-stale') : 'live';
|
|
16
37
|
const watermark = readResetWatermark();
|
|
17
38
|
let usd = 0;
|
|
18
39
|
let priced = true;
|
|
@@ -48,8 +69,11 @@ export function summarizeUsage(rows) {
|
|
|
48
69
|
byDay: session.byDay,
|
|
49
70
|
byDayModel: session.byDayModel,
|
|
50
71
|
byTool,
|
|
51
|
-
recent: session.recent
|
|
52
|
-
|
|
72
|
+
recent: session.recent.map((r) => ({
|
|
73
|
+
...r,
|
|
74
|
+
est: usdCost({ input: r.i, output: r.o, cacheRead: r.cr ?? 0, cacheWrite: r.cw ?? 0 }, r.m).usd,
|
|
75
|
+
})),
|
|
76
|
+
rtkGain: readRtkGain(RTK_COMMAND_ROWS, watermark || undefined),
|
|
53
77
|
usd,
|
|
54
78
|
priced,
|
|
55
79
|
savedUsd,
|
|
@@ -57,7 +81,9 @@ export function summarizeUsage(rows) {
|
|
|
57
81
|
co2g,
|
|
58
82
|
energyWh,
|
|
59
83
|
version: PACKAGE_VERSION,
|
|
60
|
-
|
|
84
|
+
currency,
|
|
85
|
+
source,
|
|
86
|
+
paths: { ledger: ledgerPath(), sessions: sessionsDir(), usageDb: usageDbPath() },
|
|
61
87
|
};
|
|
62
88
|
}
|
|
63
89
|
function fmt(n) {
|
|
@@ -87,8 +113,8 @@ function pad(s, n) {
|
|
|
87
113
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
88
114
|
}
|
|
89
115
|
// Box-drawing table, plain text (piped-safe, byte-stable). Numeric columns
|
|
90
|
-
// right-align; long cells truncate with an ellipsis.
|
|
91
|
-
function
|
|
116
|
+
// right-align; long cells truncate with an ellipsis. Shared with cli/settings.ts.
|
|
117
|
+
function textTable(headers, rows, right = [], maxW = 32) {
|
|
92
118
|
const cells = [headers, ...rows].map((r) => r.map((c) => (c.length > maxW ? c.slice(0, maxW - 1) + '…' : c)));
|
|
93
119
|
const widths = headers.map((_, i) => Math.max(...cells.map((r) => r[i].length)));
|
|
94
120
|
const line = (l, m, r) => l + widths.map((w) => '─'.repeat(w + 2)).join(m) + r;
|
|
@@ -100,14 +126,14 @@ function table(headers, rows, right = [], maxW = 32) {
|
|
|
100
126
|
return out.map((l) => ' ' + l);
|
|
101
127
|
}
|
|
102
128
|
function printReport(report) {
|
|
103
|
-
console.log(`\n=== Tersio Usage v${report.version} · ${fmt(report.messages)} msgs ===`);
|
|
129
|
+
console.log(`\n=== Tersio Usage v${report.version} · ${fmt(report.messages)} msgs · ${report.source} ===`);
|
|
104
130
|
if (report.empty) {
|
|
105
131
|
console.log(' No ledger rows or session tokens yet — run session commands first.');
|
|
106
132
|
return;
|
|
107
133
|
}
|
|
108
134
|
const t = report.tokens;
|
|
109
135
|
console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read${t.cacheWrite > 0 ? ` · ${fmt(t.cacheWrite)} written` : ''}`);
|
|
110
|
-
console.log(` COST
|
|
136
|
+
console.log(` COST ${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)`);
|
|
111
137
|
const models = Object.entries(report.byModel).filter(([, b]) => b.input + b.output + b.cacheRead + b.cacheWrite > 0).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output)).slice(0, 8);
|
|
112
138
|
if (models.length) {
|
|
113
139
|
console.log(' BY MODEL');
|
|
@@ -116,9 +142,9 @@ function printReport(report) {
|
|
|
116
142
|
const mt = b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
117
143
|
const hit = b.input + b.cacheRead ? (b.cacheRead / (b.input + b.cacheRead)) * 100 : 0;
|
|
118
144
|
const usd = report.byModelUsd[model] ?? 0;
|
|
119
|
-
return [model,
|
|
145
|
+
return [displayModelId(model), fmt(b.input), fmt(b.output), `${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`, formatCurrency(usd, report.currency), `${hit.toFixed(1)}%`, `${bar(mt / top, 8)} ${fmtShort(mt)}`];
|
|
120
146
|
});
|
|
121
|
-
for (const l of
|
|
147
|
+
for (const l of textTable(['Model', 'Input', 'Output', 'Cache r/w', report.currency, 'Hit', 'Share'], mrows, [false, true, true, true, true, true, false])) {
|
|
122
148
|
console.log(l);
|
|
123
149
|
}
|
|
124
150
|
}
|
|
@@ -130,7 +156,7 @@ function printReport(report) {
|
|
|
130
156
|
if (cmdRows.length) {
|
|
131
157
|
const g = report.rtkGain;
|
|
132
158
|
const scope = g.commands
|
|
133
|
-
? ` COMMAND TOOLS ${fmt(
|
|
159
|
+
? ` COMMAND TOOLS ${fmt(cmdRows.length)} commands · ${fmt(g.commands)} runs · ${fmtShort(g.saved)} saved`
|
|
134
160
|
: ' COMMAND TOOLS';
|
|
135
161
|
console.log(scope);
|
|
136
162
|
const top = cmdRows.reduce((m, r) => Math.max(m, r.count), 1);
|
|
@@ -143,7 +169,7 @@ function printReport(report) {
|
|
|
143
169
|
r.avgMs === null ? '–' : fmtMs(r.avgMs),
|
|
144
170
|
bar(r.count / top, 8),
|
|
145
171
|
]);
|
|
146
|
-
for (const l of
|
|
172
|
+
for (const l of textTable(['#', 'Tool/Command', 'Count', 'Saved', 'Avg%', 'Time', 'Impact'], crows, [true, false, true, true, true, true, false], 30)) {
|
|
147
173
|
console.log(l);
|
|
148
174
|
}
|
|
149
175
|
}
|
|
@@ -152,4 +178,4 @@ async function runUsage() {
|
|
|
152
178
|
const report = await withInteractiveSpinner('Reading usage ledger', async () => summarizeUsage(readUsage()));
|
|
153
179
|
printReport(report);
|
|
154
180
|
}
|
|
155
|
-
export { runUsage };
|
|
181
|
+
export { runUsage, textTable };
|