@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.
@@ -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
- // cli/usage.ts — ledger + session-token usage report, tokscale-style.
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
- const session = importSessionTokens();
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
- rtkGain: readRtkGain(10, watermark || undefined),
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
- paths: { ledger: ledgerPath(), sessions: sessionsDir() },
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 table(headers, rows, right = [], maxW = 32) {
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 $${report.usd.toFixed(2)}${report.priced ? '' : ' (includes default pricing)'} · ~$${report.savedUsd.toFixed(2)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)`);
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, `in ${fmt(b.input)}`, `out ${fmt(b.output)}`, `${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`, `$${usd.toFixed(2)}`, `${hit.toFixed(1)}%`, `${bar(mt / top, 8)} ${fmtShort(mt)}`];
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 table(['Model', 'Input', 'Output', 'Cache r/w', 'USD', 'Hit', 'Share'], mrows, [false, true, true, true, true, true, false])) {
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(report.byTool.length)} tools · ${fmt(g.commands)} commands · ${fmtShort(g.saved)} saved`
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 table(['#', 'Tool/Command', 'Count', 'Saved', 'Avg%', 'Time', 'Impact'], crows, [true, false, true, true, true, true, false], 30)) {
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 };