@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/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 };
@@ -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,14 +1,11 @@
1
- // cli/usage.ts — ledger + session-token usage report, tokscale-style.
2
- import path from 'node:path';
3
- 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';
4
2
  import { readRtkGain } from '../extensions/shared/rtk-gain.js';
3
+ import { readUsageDb, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
5
4
  import { withInteractiveSpinner } from './interactive.js';
6
- import { PACKAGE_VERSION } from './common.js';
7
- // RTK's history is machine-wide, so every project's commands have to reach
8
- // the table or a busy repo silently stands in for all of them. The old cap of
9
- // 10 rows ranked by tokens saved hid whole projects whose commands save little
10
- // to nothing; 2000 groups covers a long history (~1.7k today) while still
11
- // bounding /data.json.
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.
12
9
  const RTK_COMMAND_ROWS = 2000;
13
10
  export function summarizeUsage(rows) {
14
11
  refreshPricesIfStale();
@@ -19,7 +16,24 @@ export function summarizeUsage(rows) {
19
16
  detailCounts[r.detail] = (detailCounts[r.detail] ?? 0) + 1;
20
17
  }
21
18
  const byDetail = Object.entries(detailCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
22
- 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';
23
37
  const watermark = readResetWatermark();
24
38
  let usd = 0;
25
39
  let priced = true;
@@ -67,7 +81,9 @@ export function summarizeUsage(rows) {
67
81
  co2g,
68
82
  energyWh,
69
83
  version: PACKAGE_VERSION,
70
- paths: { ledger: ledgerPath(), sessions: sessionsDir() },
84
+ currency,
85
+ source,
86
+ paths: { ledger: ledgerPath(), sessions: sessionsDir(), usageDb: usageDbPath() },
71
87
  };
72
88
  }
73
89
  function fmt(n) {
@@ -97,8 +113,8 @@ function pad(s, n) {
97
113
  return s.length >= n ? s : s + ' '.repeat(n - s.length);
98
114
  }
99
115
  // Box-drawing table, plain text (piped-safe, byte-stable). Numeric columns
100
- // right-align; long cells truncate with an ellipsis.
101
- 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) {
102
118
  const cells = [headers, ...rows].map((r) => r.map((c) => (c.length > maxW ? c.slice(0, maxW - 1) + '…' : c)));
103
119
  const widths = headers.map((_, i) => Math.max(...cells.map((r) => r[i].length)));
104
120
  const line = (l, m, r) => l + widths.map((w) => '─'.repeat(w + 2)).join(m) + r;
@@ -110,14 +126,14 @@ function table(headers, rows, right = [], maxW = 32) {
110
126
  return out.map((l) => ' ' + l);
111
127
  }
112
128
  function printReport(report) {
113
- 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} ===`);
114
130
  if (report.empty) {
115
131
  console.log(' No ledger rows or session tokens yet — run session commands first.');
116
132
  return;
117
133
  }
118
134
  const t = report.tokens;
119
135
  console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read${t.cacheWrite > 0 ? ` · ${fmt(t.cacheWrite)} written` : ''}`);
120
- 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.)`);
121
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);
122
138
  if (models.length) {
123
139
  console.log(' BY MODEL');
@@ -126,36 +142,34 @@ function printReport(report) {
126
142
  const mt = b.input + b.output + b.cacheRead + b.cacheWrite;
127
143
  const hit = b.input + b.cacheRead ? (b.cacheRead / (b.input + b.cacheRead)) * 100 : 0;
128
144
  const usd = report.byModelUsd[model] ?? 0;
129
- 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)}`];
130
146
  });
131
- 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])) {
132
148
  console.log(l);
133
149
  }
134
150
  }
135
- const cmdRows = report.byTool.map(([tool, n]) => ({ name: tool, project: '', count: n, saved: null, avgPct: null, avgMs: null }));
151
+ const cmdRows = report.byTool.map(([tool, n]) => ({ name: tool, count: n, saved: null, avgPct: null, avgMs: null }));
136
152
  for (const r of report.rtkGain.byCommand) {
137
- cmdRows.push({ name: r.command, project: r.project, count: r.count, saved: r.saved, avgPct: r.avgPct, avgMs: r.avgMs });
153
+ cmdRows.push({ name: r.command, count: r.count, saved: r.saved, avgPct: r.avgPct, avgMs: r.avgMs });
138
154
  }
139
155
  cmdRows.sort((a, b) => b.count - a.count);
140
156
  if (cmdRows.length) {
141
157
  const g = report.rtkGain;
142
- const projects = new Set(g.byCommand.map((r) => r.project).filter(Boolean));
143
158
  const scope = g.commands
144
- ? ` COMMAND TOOLS ${fmt(report.byTool.length)} tools · ${fmt(g.commands)} commands · ${fmtShort(g.saved)} saved · ${fmt(projects.size)} projects`
159
+ ? ` COMMAND TOOLS ${fmt(cmdRows.length)} commands · ${fmt(g.commands)} runs · ${fmtShort(g.saved)} saved`
145
160
  : ' COMMAND TOOLS';
146
161
  console.log(scope);
147
162
  const top = cmdRows.reduce((m, r) => Math.max(m, r.count), 1);
148
163
  const crows = cmdRows.slice(0, 15).map((r, idx) => [
149
164
  String(idx + 1),
150
165
  r.name,
151
- r.project ? path.basename(r.project) : '–',
152
166
  fmt(r.count),
153
167
  r.saved === null ? '–' : fmtShort(r.saved),
154
168
  r.avgPct === null ? '–' : `${r.avgPct.toFixed(1)}%`,
155
169
  r.avgMs === null ? '–' : fmtMs(r.avgMs),
156
170
  bar(r.count / top, 8),
157
171
  ]);
158
- for (const l of table(['#', 'Tool/Command', 'Project', 'Count', 'Saved', 'Avg%', 'Time', 'Impact'], crows, [true, false, 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)) {
159
173
  console.log(l);
160
174
  }
161
175
  }
@@ -164,4 +178,4 @@ async function runUsage() {
164
178
  const report = await withInteractiveSpinner('Reading usage ledger', async () => summarizeUsage(readUsage()));
165
179
  printReport(report);
166
180
  }
167
- export { runUsage };
181
+ export { runUsage, textTable };