@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/README.md +2 -2
- package/cli/common.js +4 -4
- package/cli/dashboard.html +560 -184
- package/cli/dashboard.js +4 -4
- package/cli/doctor.js +3 -1
- package/cli/install.js +11 -11
- package/cli/update.js +9 -6
- package/cli/usage.js +17 -3
- package/extensions/shared/usage-ledger.js +35 -1
- package/extensions/tersio-commands/index.js +2 -2
- package/package.json +1 -1
- package/tersio.js +6 -6
package/cli/dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli/dashboard.ts —
|
|
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()}; } })
|
|
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]
|
|
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]
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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,
|
|
37
|
-
//
|
|
38
|
-
|
|
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:
|
|
51
|
-
|
|
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', ['
|
|
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
|
|
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
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,
|
|
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
|
-
|
|
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> (
|
|
39
|
-
--open (
|
|
40
|
-
--export <file> (
|
|
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 (
|
|
77
|
+
if (gain) {
|
|
78
78
|
await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport });
|
|
79
79
|
closeRL();
|
|
80
80
|
return;
|