@krtclcdy/tersio 2.7.0 → 2.9.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/cli/dashboard.js CHANGED
@@ -1,4 +1,4 @@
1
- // cli/dashboard.ts — localhost gain dashboard. Serves the single plain-HTML
1
+ // cli/dashboard.ts — gain dashboard. Serves the single plain-HTML
2
2
  // template (no deps, no build) plus a /data.json endpoint from the ledger.
3
3
  // Binds 127.0.0.1 only; --export writes a file://-ready file instead.
4
4
  // Note: no Promise.withResolvers here — engines still allow Node 20.12,
@@ -31,11 +31,11 @@ async function runDashboard(options) {
31
31
  if (options.exportFile) {
32
32
  const html = await templateHtml();
33
33
  const inline = html
34
- .replace("fetch('data.json')", `Promise.resolve({ json: function () { return ${dataJson()}; } }).then(function (r) { return r.json(); }).then`)
34
+ .replace("fetch('data.json')", `Promise.resolve({ json: function () { return ${dataJson()}; } })`)
35
35
  .replace('href="brand.webp"', `href="${await faviconDataUri()}"`)
36
36
  .replace('src="brand.webp"', `src="${await faviconDataUri()}"`);
37
37
  await fs.writeFile(options.exportFile, inline, 'utf8');
38
- console.log(`[ok] dashboard exported → ${options.exportFile}`);
38
+ console.log(`[ok] gain exported → ${options.exportFile}`);
39
39
  return;
40
40
  }
41
41
  const html = await withInteractiveSpinner('Loading dashboard template', templateHtml);
@@ -64,7 +64,7 @@ async function runDashboard(options) {
64
64
  const address = server.address();
65
65
  const port = typeof address === 'object' && address ? address.port : options.port;
66
66
  const url = `http://127.0.0.1:${port}`;
67
- console.log(`[ok] dashboard live → ${url} (Ctrl-C to stop)`);
67
+ console.log(`[ok] gain live → ${url} (Ctrl-C to stop)`);
68
68
  if (options.open)
69
69
  openBrowser(url);
70
70
  });
package/cli/doctor.js CHANGED
@@ -101,8 +101,10 @@ async function runDoctor() {
101
101
  section('Environment');
102
102
  check('Node', true, process.version);
103
103
  check('OMP CLI', ompVersion !== null, ompVersion ?? '');
104
- if (updateVersion)
104
+ if (typeof updateVersion === 'string')
105
105
  warnLine('Tersio CLI', `${updateVersion} available — run tersio update`);
106
+ else if (updateVersion === 'unknown')
107
+ warnLine('Tersio CLI', `${PACKAGE_VERSION} (version check unreachable)`);
106
108
  else
107
109
  check('Tersio CLI', true, PACKAGE_VERSION);
108
110
  console.log(` Home: ${HOME}`);
package/cli/install.js CHANGED
@@ -3,7 +3,7 @@ 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, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, SCOPE_MAP, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, scopeFlag, verbose, yes, dashboardExport, dashboardOpen, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
6
+ import { BUN_BIN_DIR, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, SCOPE_MAP, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, scopeFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
7
7
  import { ask, askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
8
8
  import { checkForUpdate, runLatestUpdate } from './update.js';
9
9
  import { runUninstall } from './uninstall.js';
@@ -550,7 +550,7 @@ let updatePromptDone = false;
550
550
  // Scripts, pipes, --yes, and --dry-run keep the old straight-to-install path.
551
551
  async function runCommandMenu() {
552
552
  const newer = await checkForUpdate();
553
- if (newer && !dryRun) {
553
+ if (typeof newer === 'string' && !dryRun) {
554
554
  const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
555
555
  if (answer.status === 'confirmed' && answer.value) {
556
556
  await runLatestUpdate();
@@ -571,7 +571,6 @@ async function runCommandMenu() {
571
571
  { value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
572
572
  { value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
573
573
  { value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
574
- { value: 'dashboard', label: 'Serve dashboard', hint: 'localhost server, no browser' },
575
574
  { value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
576
575
  ], 'install');
577
576
  if (choice.status !== 'selected') {
@@ -583,8 +582,13 @@ async function runCommandMenu() {
583
582
  await runInstall();
584
583
  break;
585
584
  case 'check': {
586
- const latest = await checkForUpdate();
587
- console.log(latest ? ` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})` : ` [ok] tersio ${PACKAGE_VERSION} is the latest`);
585
+ const latest = await checkForUpdate(true);
586
+ if (typeof latest === 'string')
587
+ console.log(` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})`);
588
+ else if (latest === 'unknown')
589
+ console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
590
+ else
591
+ console.log(` [ok] tersio ${PACKAGE_VERSION} is the latest`);
588
592
  closeRL();
589
593
  break;
590
594
  }
@@ -601,11 +605,7 @@ async function runCommandMenu() {
601
605
  closeRL();
602
606
  break;
603
607
  case 'gain':
604
- await runDashboard({ port: dashboardPort, open: true, exportFile: null });
605
- closeRL();
606
- break;
607
- case 'dashboard':
608
- await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport });
608
+ await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
609
609
  closeRL();
610
610
  break;
611
611
  case 'uninstall':
@@ -632,7 +632,7 @@ async function runInstall() {
632
632
  // the apply-update payload, which is itself an update run.
633
633
  if (tty() && !applyUpdate && command !== 'uninstall') {
634
634
  const newer = await checkForUpdate();
635
- if (newer) {
635
+ if (typeof newer === 'string') {
636
636
  // Bare `tersio` with an update pending: offer it now (Y/n) instead of
637
637
  // burying the banner above the install prompts. Yes runs the full
638
638
  // update and stops here — the fresh binary owns what follows.
package/cli/update.js CHANGED
@@ -33,13 +33,15 @@ async function latestPublishedVersion() {
33
33
  return null;
34
34
  }
35
35
  }
36
- // Returns a newer published version, or null. Reads the registry at most once
37
- // per TTL (cached under OMP_PLUGINS_DIR) and stays silent on failure.
38
- async function checkForUpdate() {
36
+ // Returns a newer published version, null when up to date, or 'unknown' when
37
+ // the registry cannot be reached. Reads the registry at most once per TTL
38
+ // (cached under OMP_PLUGINS_DIR) unless force skips the cache — an explicit
39
+ // "check for updates" must never trust a stale cache.
40
+ async function checkForUpdate(force = false) {
39
41
  const cachePath = path.join(OMP_PLUGINS_DIR, 'tersio-update-check.json');
40
42
  const cached = parseJsonObject(await readTextIfExists(cachePath));
41
43
  const cacheFresh = typeof cached?.lastCheck === 'number' && Date.now() - cached.lastCheck < UPDATE_CHECK_TTL_MS;
42
- if (cacheFresh && cached?.latest)
44
+ if (!force && cacheFresh && cached?.latest)
43
45
  return newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : null;
44
46
  const latest = await latestPublishedVersion();
45
47
  if (latest) {
@@ -47,8 +49,9 @@ async function checkForUpdate() {
47
49
  await fs.writeFile(cachePath, JSON.stringify({ latest, lastCheck: Date.now() }) + '\n', 'utf8').catch(() => { });
48
50
  return newerThan(latest, PACKAGE_VERSION) ? latest : null;
49
51
  }
50
- // Registry unreachable: fall back to a stale cache rather than staying silent.
51
- return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : null;
52
+ // Registry unreachable: a stale cache naming a newer release is still
53
+ // actionable; otherwise report unknown so callers never claim "latest".
54
+ return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : 'unknown';
52
55
  }
53
56
  // Race a probe against a timeout; slow or failing probes resolve null so the
54
57
  // update plan stays best-effort and never blocks the update itself.
package/cli/usage.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // cli/usage.ts — ledger + session-token usage report, tokscale-style.
2
- import { co2Grams, importSessionTokens, readUsage, usdCost, } from '../extensions/shared/usage-ledger.js';
2
+ import { co2Grams, importSessionTokens, priceFor, readUsage, usdCost, } from '../extensions/shared/usage-ledger.js';
3
3
  import { withInteractiveSpinner } from './interactive.js';
4
+ import { PACKAGE_VERSION } from './common.js';
4
5
  export function summarizeUsage(rows) {
5
6
  const byKind = {};
6
7
  const detailCounts = {};
@@ -12,12 +13,15 @@ export function summarizeUsage(rows) {
12
13
  const session = importSessionTokens();
13
14
  let usd = 0;
14
15
  let priced = true;
16
+ let savedUsd = 0;
15
17
  for (const [model, t] of Object.entries(session.byModel)) {
16
18
  const c = usdCost(t, model);
17
19
  usd += c.usd;
18
20
  if (!c.priced)
19
21
  priced = false;
22
+ savedUsd += (t.cacheRead / 1e6) * priceFor(model).price.input;
20
23
  }
24
+ const byTool = Object.entries(session.byTool).sort((a, b) => b[1] - a[1]).slice(0, 10);
21
25
  return {
22
26
  total: rows.length,
23
27
  byKind,
@@ -28,9 +32,14 @@ export function summarizeUsage(rows) {
28
32
  tokens: session.totals,
29
33
  byModel: session.byModel,
30
34
  byDay: session.byDay,
35
+ byDayModel: session.byDayModel,
36
+ byTool,
31
37
  usd,
32
38
  priced,
39
+ savedUsd,
40
+ costMeasured: session.costMeasured,
33
41
  co2g: co2Grams(session.totals.output),
42
+ version: PACKAGE_VERSION,
34
43
  };
35
44
  }
36
45
  function fmt(n) {
@@ -44,7 +53,7 @@ function printReport(report) {
44
53
  }
45
54
  const t = report.tokens;
46
55
  console.log(` Tokens input ${fmt(t.input)} · output ${fmt(t.output)} · cache read ${fmt(t.cacheRead)} · cache write ${fmt(t.cacheWrite)} (${report.messages} assistant messages)`);
47
- console.log(` Cost $${report.usd.toFixed(2)}${report.priced ? '' : ' (includes default pricing for unknown models)'} · CO2 ~${report.co2g.toFixed(1)}g (est.)`);
56
+ console.log(` Cost $${report.usd.toFixed(2)}${report.priced ? '' : ' (includes default pricing for unknown models)'} · ~$${report.savedUsd.toFixed(2)} saved by cache (est.) · CO2 ~${report.co2g.toFixed(1)}g (est.)`);
48
57
  const models = Object.entries(report.byModel).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output)).slice(0, 5);
49
58
  if (models.length) {
50
59
  console.log(' By model');
@@ -52,6 +61,12 @@ function printReport(report) {
52
61
  console.log(` ${model}: in ${fmt(b.input)} · out ${fmt(b.output)} · cache r/w ${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`);
53
62
  }
54
63
  }
64
+ if (report.byTool.length) {
65
+ console.log(' Top tools');
66
+ for (const [tool, n] of report.byTool) {
67
+ console.log(` ${n}× ${tool}`);
68
+ }
69
+ }
55
70
  if (report.total) {
56
71
  console.log(' Activity');
57
72
  for (const [kind, n] of Object.entries(report.byKind)) {
@@ -61,7 +76,6 @@ function printReport(report) {
61
76
  for (const [detail, n] of report.byDetail) {
62
77
  console.log(` ${n}× ${detail}`);
63
78
  }
64
- console.log(` Total rows: ${report.total} · last write: ${report.lastWrite}`);
65
79
  }
66
80
  }
67
81
  async function runUsage() {
@@ -64,8 +64,11 @@ function walkJsonl(dir, out, cap) {
64
64
  export function importSessionTokens() {
65
65
  const byModel = {};
66
66
  const byDay = {};
67
+ const byDayModel = {};
68
+ const byTool = {};
67
69
  const totals = zeroBreakdown();
68
70
  let messages = 0;
71
+ let costMeasured = 0;
69
72
  const files = [];
70
73
  walkJsonl(sessionsDir(), files, 2000);
71
74
  for (const file of files) {
@@ -92,13 +95,44 @@ export function importSessionTokens() {
92
95
  if (day) {
93
96
  byDay[day] ??= zeroBreakdown();
94
97
  addInto(byDay[day], msg.usage);
98
+ const n = msg.usage;
99
+ const sum = ['input', 'output', 'cacheRead', 'cacheWrite'].reduce((a, k) => a + (typeof n[k] === 'number' && Number.isFinite(n[k]) ? Math.floor(n[k]) : 0), 0);
100
+ if (sum > 0) {
101
+ byDayModel[day] ??= {};
102
+ byDayModel[day][model] = (byDayModel[day][model] ?? 0) + sum;
103
+ }
104
+ }
105
+ if (typeof msg.usage.cost === 'number' && Number.isFinite(msg.usage.cost))
106
+ costMeasured += msg.usage.cost;
107
+ for (const part of msg.content ?? []) {
108
+ if (part?.type === 'toolCall' && typeof part.name === 'string' && part.name) {
109
+ const key = part.name === 'bash' ? `bash:${leadBinary(part.arguments?.command)}` : part.name;
110
+ byTool[key] = (byTool[key] ?? 0) + 1;
111
+ }
95
112
  }
96
113
  messages += 1;
97
114
  }
98
115
  catch { /* skip corrupt lines */ }
99
116
  }
100
117
  }
101
- return { messages, totals, byModel, byDay };
118
+ return { messages, totals, byModel, byDay, byDayModel, byTool, costMeasured };
119
+ }
120
+ // Lead binary of a shell string: first segment head past `cd` chains and
121
+ // VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
122
+ const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
123
+ function leadBinary(command) {
124
+ if (typeof command !== 'string' || !command.trim())
125
+ return 'bash';
126
+ for (const seg of command.split(/&&|;|\n|\|(?!\|)/)) {
127
+ const toks = seg.trim().split(/\s+/).filter(Boolean);
128
+ let i = 0;
129
+ while (i < toks.length && (toks[i] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i])))
130
+ i++;
131
+ const head = (toks[i] ?? '').replace(/^['"]|['"]$/g, '').split('/').pop() ?? '';
132
+ if (head && !SKIP_HEADS.includes(head))
133
+ return head;
134
+ }
135
+ return 'bash';
102
136
  }
103
137
  const PRICE_TABLE = [
104
138
  { match: ':free', price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
@@ -43,7 +43,7 @@ function usageSummary() {
43
43
  async function openGainDashboard(pi, ctx) {
44
44
  const file = path.join(os.tmpdir(), `tersio-gain-${Date.now()}.html`);
45
45
  try {
46
- const exported = await pi.exec?.('tersio', ['dashboard', '--export', file], { cwd: ctx?.cwd });
46
+ const exported = await pi.exec?.('tersio', ['gain', '--export', file], { cwd: ctx?.cwd });
47
47
  if (!exported || exported.code !== 0)
48
48
  throw new Error((exported?.stderr || 'export failed').trim());
49
49
  const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
@@ -51,7 +51,7 @@ async function openGainDashboard(pi, ctx) {
51
51
  ctx?.ui?.notify?.(`tersio gain: dashboard opened in your browser.`, 'info');
52
52
  }
53
53
  catch (e) {
54
- ctx?.ui?.notify?.(`tersio gain: could not open dashboard (${e.message}). Run 'tersio dashboard --open' in a shell.`, 'warning');
54
+ ctx?.ui?.notify?.(`tersio gain: could not open dashboard (${e.message}). Run 'tersio gain --open' in a shell.`, 'warning');
55
55
  }
56
56
  }
57
57
  export default function tersioCommandsExtension(pi) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "✂️ Token-saving OMP add-ons: terse Caveman replies, compact RTK shell output, lean Ponytail code calls — one-command combo presets.",
5
5
  "keywords": [
6
6
  "omp",
package/tersio.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // tersio.ts — Install Tersio (caveman/rtk/ponytail) add-ons on any OMP device.
3
3
  // Usage: node tersio.js [install|update|reinstall|doctor|uninstall|version|help] [options]
4
4
  // Requires: node/npm and omp CLI
5
- import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, dashboardExport, dashboardOpen, dashboardPort, doctor, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
5
+ import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, gain, dashboardExport, dashboardOpen, dashboardPort, doctor, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
6
6
  import { closeRL } from './cli/interactive.js';
7
7
  import { runInstall } from './cli/install.js';
8
8
  import { runDoctor } from './cli/doctor.js';
@@ -19,7 +19,7 @@ Commands:
19
19
  reinstall Clean and reinstall the user-scope add-ons
20
20
  doctor Check the current installation
21
21
  usage Ledger-backed usage + savings report
22
- dashboard Serve the gain dashboard (localhost only)
22
+ gain Open the gain dashboard (localhost only)
23
23
  uninstall Remove the managed extensions
24
24
  version Print the package version
25
25
  help Show this help
@@ -35,9 +35,9 @@ Options:
35
35
  --yes, -y
36
36
  --dry-run
37
37
  --verbose
38
- --port <n> (dashboard: pin port, default ephemeral)
39
- --open (dashboard: open browser)
40
- --export <file> (dashboard: write HTML file instead of serving)
38
+ --port <n> (gain: pin port, default ephemeral)
39
+ --open (gain: open browser)
40
+ --export <file> (gain: write HTML file instead of serving)
41
41
  --version, -v
42
42
  --help, -h`);
43
43
  }
@@ -74,7 +74,7 @@ async function main() {
74
74
  closeRL();
75
75
  return;
76
76
  }
77
- if (dashboard) {
77
+ if (gain) {
78
78
  await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport });
79
79
  closeRL();
80
80
  return;