@krtclcdy/tersio 2.21.0 → 2.22.1
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 +19 -15
- package/cli/common.js +16 -4
- package/cli/currency.js +2 -2
- package/cli/dashboard.js +313 -52
- package/cli/doctor-fix.js +12 -12
- package/cli/doctor.js +21 -52
- package/cli/install.js +31 -60
- package/cli/profile.js +1 -1
- package/cli/settings.js +132 -9
- package/cli/uninstall.js +4 -4
- package/cli/update.js +10 -20
- package/cli/usage.js +1 -1
- package/dashboard/dist/index.html +65 -0
- package/extensions/ai-addons-updater/index.js +13 -30
- package/extensions/ai-addons-updater/index.ts +11 -29
- package/extensions/lib/utils.js +26 -1
- package/extensions/lib/utils.ts +28 -1
- package/extensions/shared/pricing.js +2 -2
- package/extensions/shared/pricing.ts +2 -1
- package/extensions/shared/usage-ledger.js +4 -3
- package/extensions/shared/usage-ledger.ts +4 -3
- package/extensions/shared/usage-store.js +7 -7
- package/extensions/shared/usage-store.ts +7 -6
- package/extensions/tersio-commands/index.js +10 -13
- package/extensions/tersio-commands/index.ts +10 -13
- package/package.json +22 -36
- package/tersio.js +9 -9
- package/dashboard/app.js +0 -1495
- package/dashboard/styles.css +0 -304
- package/dashboard/template.html +0 -410
- /package/dashboard/{brand.webp → dist/brand.webp} +0 -0
package/cli/doctor.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// cli/doctor.ts — installation health checks.
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR,
|
|
4
|
+
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, RTK_BINARY_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
5
|
import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
|
|
6
|
-
import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
7
6
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
8
|
-
import {
|
|
9
|
-
import { checkForUpdate } from './update.js';
|
|
7
|
+
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
10
8
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
11
9
|
async function runDoctor(recheck = false) {
|
|
12
10
|
console.log('\n=== Tersio Doctor ===');
|
|
@@ -23,60 +21,43 @@ async function runDoctor(recheck = false) {
|
|
|
23
21
|
const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
|
|
24
22
|
const rtkIndex = path.join(extDir, 'rtk-session', 'index.ts');
|
|
25
23
|
const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.ts');
|
|
26
|
-
const comboIndex = path.join(extDir, 'combo-toggle', 'index.ts');
|
|
27
|
-
const tersioIndex = path.join(extDir, 'tersio-commands', 'index.ts');
|
|
28
|
-
const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.ts');
|
|
29
|
-
const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
|
|
30
24
|
// Independent probes start concurrently; sections report in fixed order as
|
|
31
25
|
// their data settles. Every probe resolves instead of rejecting.
|
|
32
26
|
const probes = {
|
|
33
|
-
ompVersion: execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
|
|
34
27
|
agentEntries: fs.readdir(agentDir).catch(() => null),
|
|
35
28
|
extEntries: fs.readdir(extDir).catch(() => null),
|
|
36
29
|
sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.ts')),
|
|
37
30
|
configText: readTextIfExists(configPath),
|
|
38
31
|
ponytailPkgText: readTextIfExists(ponytailPkg),
|
|
39
32
|
ponytailExtText: readTextIfExists(ponytailExt),
|
|
40
|
-
pluginsPkgRaw: readTextIfExists(path.join(pluginsDir, 'package.json')),
|
|
41
|
-
selfPkgText: readTextIfExists(selfPkg),
|
|
42
33
|
rtkBinText: readTextIfExists(rtkBin),
|
|
43
34
|
rtkOmpText: readTextIfExists(path.join(extDir, 'rtk.ts')),
|
|
44
35
|
cavemanIndexText: readTextIfExists(cavemanIndex),
|
|
45
36
|
cavemanRuleText: readTextIfExists(cavemanRule),
|
|
46
37
|
rtkIndexText: readTextIfExists(rtkIndex),
|
|
47
38
|
updaterIndexText: readTextIfExists(updaterIndex),
|
|
48
|
-
comboIndexText: readTextIfExists(comboIndex),
|
|
49
|
-
tersioIndexText: readTextIfExists(tersioIndex),
|
|
50
|
-
modeReinforcementText: readTextIfExists(modeReinforcement),
|
|
51
39
|
ruleMtime: fs.stat(cavemanRule).catch(() => null),
|
|
52
40
|
rtkMtime: fs.stat(rtkBin).catch(() => null),
|
|
53
41
|
ponytailMtime: fs.stat(ponytailPkg).catch(() => null),
|
|
54
|
-
|
|
42
|
+
pricesMtime: fs.stat(pricesCachePath()).catch(() => null),
|
|
55
43
|
};
|
|
56
44
|
const rtkVersionProbe = probes.rtkBinText.then((text) => text === null ? null : execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => r.stdout.trim() || r.stderr.trim() || null, (e) => {
|
|
57
45
|
const err = e;
|
|
58
46
|
return err.stdout?.trim() || err.stderr?.trim() || null;
|
|
59
47
|
}));
|
|
60
|
-
const [[
|
|
48
|
+
const [[agentEntries, extEntries, sharedStateText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText, ponytailExtText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
61
49
|
Promise.all([
|
|
62
|
-
probes.ompVersion,
|
|
63
50
|
probes.agentEntries,
|
|
64
51
|
probes.extEntries,
|
|
65
52
|
probes.sharedStateText,
|
|
66
53
|
probes.configText,
|
|
67
|
-
probes.updateVersion,
|
|
68
54
|
]),
|
|
69
55
|
Promise.all([
|
|
70
56
|
probes.cavemanIndexText,
|
|
71
57
|
probes.rtkIndexText,
|
|
72
58
|
probes.updaterIndexText,
|
|
73
|
-
probes.comboIndexText,
|
|
74
|
-
probes.tersioIndexText,
|
|
75
|
-
probes.modeReinforcementText,
|
|
76
59
|
probes.ponytailPkgText,
|
|
77
60
|
probes.ponytailExtText,
|
|
78
|
-
probes.pluginsPkgRaw,
|
|
79
|
-
probes.selfPkgText,
|
|
80
61
|
]),
|
|
81
62
|
Promise.all([
|
|
82
63
|
probes.cavemanRuleText,
|
|
@@ -86,11 +67,18 @@ async function runDoctor(recheck = false) {
|
|
|
86
67
|
rtkVersionProbe,
|
|
87
68
|
probes.rtkOmpText,
|
|
88
69
|
probes.ponytailMtime,
|
|
70
|
+
probes.pricesMtime,
|
|
89
71
|
]),
|
|
90
72
|
]));
|
|
91
73
|
// Categorized output with a tally; success rows stay quiet (no path echoes)
|
|
92
74
|
// while failures print the expected path or fix so they stay actionable.
|
|
93
75
|
const tally = { ok: 0, missing: 0, warn: 0 };
|
|
76
|
+
function absDate(ms) {
|
|
77
|
+
const d = new Date(ms);
|
|
78
|
+
const q = (n) => String(n).padStart(2, '0');
|
|
79
|
+
const h24 = d.getHours();
|
|
80
|
+
return `${q(d.getMonth() + 1)}-${q(d.getDate())}-${d.getFullYear()}, ${q(h24 % 12 || 12)}:${q(d.getMinutes())} ${h24 >= 12 ? 'PM' : 'AM'}`;
|
|
81
|
+
}
|
|
94
82
|
function check(label, ok, detail = '') {
|
|
95
83
|
if (ok) {
|
|
96
84
|
tally.ok++;
|
|
@@ -107,13 +95,6 @@ async function runDoctor(recheck = false) {
|
|
|
107
95
|
}
|
|
108
96
|
section('Environment');
|
|
109
97
|
check('Node', true, process.version);
|
|
110
|
-
check('OMP CLI', ompVersion !== null, ompVersion ?? '');
|
|
111
|
-
if (typeof updateVersion === 'string')
|
|
112
|
-
warnLine('Tersio CLI', `${updateVersion} available — run tersio update`);
|
|
113
|
-
else if (updateVersion === 'unknown')
|
|
114
|
-
warnLine('Tersio CLI', `${PACKAGE_VERSION} (version check unreachable)`);
|
|
115
|
-
else
|
|
116
|
-
check('Tersio CLI', true, PACKAGE_VERSION);
|
|
117
98
|
section('Installation');
|
|
118
99
|
check('OMP agent dir', agentEntries !== null, agentEntries === null ? agentDir : '');
|
|
119
100
|
check('OMP extensions dir', extEntries !== null, extEntries === null ? extDir : '');
|
|
@@ -123,37 +104,25 @@ async function runDoctor(recheck = false) {
|
|
|
123
104
|
check('Caveman extension', cavemanIndexText !== null);
|
|
124
105
|
check('RTK extension', rtkIndexText !== null);
|
|
125
106
|
check('Updater extension', updaterIndexText !== null);
|
|
126
|
-
check('Combo extension', comboIndexText !== null);
|
|
127
|
-
check('Tersio commands extension', tersioIndexText !== null);
|
|
128
|
-
check('Mode reinforcement extension', modeReinforcementText !== null);
|
|
129
107
|
check('Ponytail extension', ponytailExtText !== null);
|
|
130
|
-
check('Ponytail in config.yml', (configText ?? '').includes('ponytail') && (configText ?? '').includes('pi-extension'));
|
|
131
|
-
check('Combo in config.yml', (configText ?? '').includes('combo-toggle'));
|
|
132
|
-
check('Self plugin package', selfPkgText !== null, parseJsonObject(selfPkgText)?.version ?? '');
|
|
133
|
-
const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
|
|
134
|
-
check('Self plugin in plugins/package.json', selfDep);
|
|
135
108
|
section('Usage & records');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.log(` Session transcripts (host-owned, never touched): ${sessionsDir()}`);
|
|
140
|
-
console.log(` Usage DB (tersio-owned, tersio reset clears): ${usageDbPath()}`);
|
|
141
|
-
const rtkDb = rtkDbPath();
|
|
142
|
-
const rtk = readRtkGain();
|
|
143
|
-
console.log(` RTK history (rtk-owned, never touched): ${rtkDb}${rtk.commands ? ` · ${rtk.commands} commands` : ''}`);
|
|
109
|
+
console.log(` Usage DB (tersio-owned · local hosted): ${usageDbPath()}`);
|
|
110
|
+
const pricesAge = pricesMtime ? `(pulled ${relTime(Date.now() - pricesMtime.mtimeMs)} · ${absDate(pricesMtime.mtimeMs)})` : '';
|
|
111
|
+
check('Prices feed', pricesMtime !== null, pricesAge || pricesCachePath());
|
|
144
112
|
section('Add-ons');
|
|
145
|
-
const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
|
|
113
|
+
const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
|
|
146
114
|
check('Caveman rule', cavemanRuleText !== null, ruleAge);
|
|
147
|
-
const rtkAge = rtkMtime ? `updated ${relTime(Date.now() - rtkMtime.mtimeMs)}` : '';
|
|
148
|
-
check('RTK binary', rtkBinText !== null, rtkBinText === null ? rtkBin : [rtkVersion, rtkAge].filter(Boolean).join('
|
|
115
|
+
const rtkAge = rtkMtime ? `(updated ${relTime(Date.now() - rtkMtime.mtimeMs)} · ${absDate(rtkMtime.mtimeMs)})` : '';
|
|
116
|
+
check('RTK binary', rtkBinText !== null, rtkBinText === null ? rtkBin : [rtkVersion, rtkAge].filter(Boolean).join(' '));
|
|
149
117
|
if (rtkBinText !== null && !rtkVersion)
|
|
150
118
|
warnLine('RTK version', 'unavailable — binary may not be executable');
|
|
151
119
|
const rtkRegistered = rtkOmpText !== null && (configText ?? '').includes('extensions/rtk.ts');
|
|
152
120
|
check('RTK OMP wiring (rtk.ts)', rtkOmpText !== null, rtkOmpText === null ? 'run: rtk init -g --agent omp' : '');
|
|
153
121
|
if (rtkOmpText !== null && !rtkRegistered)
|
|
154
122
|
warnLine('RTK in config.yml', 'rtk.ts not listed — OMP will not load it; rerun install');
|
|
155
|
-
const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
|
|
156
|
-
|
|
123
|
+
const ponytailAge = ponytailMtime ? `(updated ${relTime(Date.now() - ponytailMtime.mtimeMs)} · ${absDate(ponytailMtime.mtimeMs)})` : '';
|
|
124
|
+
const ponytailVer = parseJsonObject(ponytailPkgText)?.version ?? '';
|
|
125
|
+
check('Ponytail', ponytailPkgText !== null, [ponytailVer, ponytailAge].filter(Boolean).join(' '));
|
|
157
126
|
const total = tally.ok + tally.missing + tally.warn;
|
|
158
127
|
console.log(`\n Summary: ${total} checks — ✅ ${tally.ok} ok, ⚠️ ${tally.warn} warn, ❌ ${tally.missing} missing`);
|
|
159
128
|
if (!recheck && tally.missing + tally.warn > 0 && (fix || dryRun)) {
|
|
@@ -219,7 +188,7 @@ async function runDoctorFix(tally) {
|
|
|
219
188
|
{ value: 'extensions', label: 'Missing extension files', hint: 'copy sources from this CLI' },
|
|
220
189
|
{ value: 'registrations', label: 'config.yml registrations', hint: 'combo, ponytail, rtk.ts entries' },
|
|
221
190
|
{ value: 'rtk', label: 'RTK binary + wiring', hint: 'download checksum-verified binary, rtk init' },
|
|
222
|
-
{ value: 'ponytail', label: 'Ponytail package', hint: '
|
|
191
|
+
{ value: 'ponytail', label: 'Ponytail package', hint: 'bundled reinstall via tersio dep' },
|
|
223
192
|
{ value: 'cli', label: 'CLI update', hint: 'latest tersio + delegated refresh' },
|
|
224
193
|
], 'all');
|
|
225
194
|
if (picked.status !== 'selected') {
|
package/cli/install.js
CHANGED
|
@@ -31,98 +31,69 @@ const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts'
|
|
|
31
31
|
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
|
|
32
32
|
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
|
|
33
33
|
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
|
|
34
|
-
const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
|
|
35
|
-
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
36
34
|
async function stepPonytail(pluginsDir, userDir, options) {
|
|
37
35
|
if (!options.quiet)
|
|
38
|
-
console.log(' Ponytail —
|
|
36
|
+
console.log(' Ponytail — ensure bundled plugin');
|
|
39
37
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
40
38
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
41
39
|
const pkg = await readPluginsPackage(pkgPath);
|
|
42
|
-
|
|
40
|
+
// Migration: ponytail was a separate plugin row. It is now a tersio
|
|
41
|
+
// dependency, so drop the legacy row. Runs after stepSelfPlugin.
|
|
42
|
+
let migrated = false;
|
|
43
|
+
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
44
|
+
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
45
|
+
migrated = true;
|
|
46
|
+
}
|
|
43
47
|
if (options.dryRun) {
|
|
44
48
|
if (verbose && !options.quiet)
|
|
45
49
|
console.log(` [dry-run] would write ${pkgPath}`);
|
|
50
|
+
if (migrated && verbose && !options.quiet)
|
|
51
|
+
console.log(` [dry-run] would drop the separate @dietrichgebert/ponytail dependency (bundled with tersio)`);
|
|
46
52
|
}
|
|
47
|
-
else {
|
|
53
|
+
else if (migrated) {
|
|
48
54
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
55
|
+
if (!options.quiet)
|
|
56
|
+
console.log(' [migrate] dropped separate @dietrichgebert/ponytail dependency (now bundled with tersio)');
|
|
49
57
|
}
|
|
50
58
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
51
59
|
const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
|
|
52
|
-
// Fast path:
|
|
53
|
-
|
|
60
|
+
// Fast path: bundled copy already present and no refresh asked — skip
|
|
61
|
+
// network. Migration always reinstalls: the probe may hit the stale legacy
|
|
62
|
+
// copy, and only npm install prunes it into the tersio-owned one.
|
|
63
|
+
let ponytailExtExists = !options.reinstall && !options.dryRun && !migrated ? await probeExt() : false;
|
|
54
64
|
if (ponytailExtExists) {
|
|
55
|
-
debug('Ponytail pi-extension already installed; skipping network refresh');
|
|
65
|
+
debug('Bundled Ponytail pi-extension already installed; skipping network refresh');
|
|
56
66
|
}
|
|
57
67
|
else if (options.dryRun) {
|
|
58
68
|
// Dry runs preview the wiring below without touching the network.
|
|
59
69
|
if (verbose && !options.quiet)
|
|
60
|
-
console.log(
|
|
61
|
-
if (options.reinstall && verbose && !options.quiet) {
|
|
62
|
-
console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
|
|
63
|
-
}
|
|
70
|
+
console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
|
|
64
71
|
ponytailExtExists = true;
|
|
65
72
|
}
|
|
66
73
|
else {
|
|
67
|
-
//
|
|
74
|
+
// Self-plugin npm install already materialized the bundled copy.
|
|
75
|
+
// Reinstall here only when the probe still misses.
|
|
68
76
|
try {
|
|
69
|
-
await execNetwork('Installing Ponytail
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
console.log(` [warn] omp plugin install failed: ${e.message}`);
|
|
77
|
+
await execNetwork('Installing bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
|
|
73
78
|
}
|
|
74
|
-
|
|
79
|
+
catch {
|
|
75
80
|
try {
|
|
76
|
-
await execNetwork('
|
|
81
|
+
await execNetwork('Installing bundled Ponytail', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
|
|
77
82
|
}
|
|
78
83
|
catch (e) {
|
|
79
|
-
console.log(` [fail] Could not
|
|
80
|
-
console.log(
|
|
84
|
+
console.log(` [fail] Could not install bundled ponytail: ${e.message}`);
|
|
85
|
+
console.log(' [hint] Manual: cd ~/.omp/plugins && npm install --no-audit --no-fund');
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
|
-
// Verify the pi-extension/index.js actually exists
|
|
84
88
|
ponytailExtExists = await probeExt();
|
|
85
|
-
// Fallback: try bun install or npm install
|
|
86
|
-
if (!ponytailExtExists) {
|
|
87
|
-
if (!options.quiet)
|
|
88
|
-
console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
|
|
89
|
-
try {
|
|
90
|
-
await execNetwork('Installing Ponytail dependencies', 'npm', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
try {
|
|
94
|
-
await execNetwork('Installing Ponytail dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
95
|
-
}
|
|
96
|
-
catch (e2) {
|
|
97
|
-
console.log(` [fail] Could not install ponytail: ${e2.message}`);
|
|
98
|
-
console.log(' [hint] Manual: cd ~/.omp/plugins && npm install');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
ponytailExtExists = await probeExt();
|
|
102
|
-
}
|
|
103
|
-
// Last-resort fallback: git clone the repo into node_modules
|
|
104
|
-
if (!ponytailExtExists) {
|
|
105
|
-
if (!options.quiet)
|
|
106
|
-
console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
|
|
107
|
-
try {
|
|
108
|
-
const dest = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
|
|
109
|
-
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
110
|
-
await execNetwork('Cloning Ponytail repository', 'git', ['clone', '--depth', '1', 'https://github.com/DietrichGebert/ponytail.git', dest], { timeout: 180000 });
|
|
111
|
-
ponytailExtExists = await probeExt();
|
|
112
|
-
}
|
|
113
|
-
catch (e3) {
|
|
114
|
-
console.log(` [fail] git clone failed: ${e3.message}`);
|
|
115
|
-
console.log(' [hint] Install git or check network: https://github.com/DietrichGebert/ponytail');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
89
|
}
|
|
119
90
|
if (!ponytailExtExists) {
|
|
120
|
-
console.log(' [skip] Ponytail pi-extension/index.js still not found — skill-only mode');
|
|
91
|
+
console.log(' [skip] Bundled Ponytail pi-extension/index.js still not found — skill-only mode');
|
|
121
92
|
console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
|
|
122
93
|
}
|
|
123
94
|
else if (!options.dryRun && !options.quiet) {
|
|
124
95
|
// Wire extension into config.yml so /ponytail command loads
|
|
125
|
-
debug('Ponytail pi-extension found');
|
|
96
|
+
debug('Bundled Ponytail pi-extension found');
|
|
126
97
|
}
|
|
127
98
|
// Still set Ponytail config defaults even without the extension command
|
|
128
99
|
const configPath = path.join(userDir, 'config.yml');
|
|
@@ -513,7 +484,7 @@ async function runCommandMenu() {
|
|
|
513
484
|
{ value: 'reinstall', label: 'Reinstall', hint: 'clean and reinstall the add-ons, Ponytail package kept' },
|
|
514
485
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
515
486
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
516
|
-
{ value: '
|
|
487
|
+
{ value: 'dashboard', label: 'Dashboard', hint: 'open the report in your browser' },
|
|
517
488
|
{ value: 'reset', label: 'Reset statistics', hint: 'clear statistics; transcripts and RTK history stay' },
|
|
518
489
|
{ value: 'settings', label: 'Settings', hint: 'defaults: combo, caveman, rtk, ponytail, currency' },
|
|
519
490
|
{ value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
|
|
@@ -574,7 +545,7 @@ async function runCommandMenu() {
|
|
|
574
545
|
await runUsage();
|
|
575
546
|
closeRL();
|
|
576
547
|
break;
|
|
577
|
-
case '
|
|
548
|
+
case 'dashboard':
|
|
578
549
|
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
|
|
579
550
|
closeRL();
|
|
580
551
|
break;
|
|
@@ -671,9 +642,9 @@ async function runInstall(overrides = {}) {
|
|
|
671
642
|
}
|
|
672
643
|
};
|
|
673
644
|
await capture('shared', () => stepSharedSessionState(userExtDir, options));
|
|
674
|
-
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
|
|
675
645
|
let selfPlugin = false;
|
|
676
646
|
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options); });
|
|
647
|
+
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
|
|
677
648
|
const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
678
649
|
await capture('rtk', () => stepRtk(BUN_BIN_DIR, options));
|
|
679
650
|
await capture('rtk session', () => stepRtkSession(userExtDir, options));
|
package/cli/profile.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// cli/profile.ts — session-start defaults profile (combo/caveman/rtk/ponytail)
|
|
2
|
-
// plus the display-currency default for usage/
|
|
2
|
+
// plus the display-currency default for usage/dashboard reports.
|
|
3
3
|
// Single source for reading/writing the omp plugin settings lock entry.
|
|
4
4
|
// Extracted from cli/install.ts so both install and settings share it.
|
|
5
5
|
import { promises as fs } from 'node:fs';
|
package/cli/settings.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// cli/settings.ts — view/edit session-start defaults (combo/caveman/rtk/ponytail)
|
|
2
|
-
// plus the display-currency default for usage/
|
|
2
|
+
// plus the display-currency default for usage/dashboard reports.
|
|
3
3
|
// Non-interactive: `tersio settings --combo-default balanced` (plus per-mode
|
|
4
4
|
// override flags and --currency). Interactive: clack selects seeded from the
|
|
5
5
|
// stored profile.
|
|
6
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';
|
|
7
|
+
import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, } from './common.js';
|
|
8
8
|
import { CURRENCY_CODES } from './currency.js';
|
|
9
9
|
import { textTable } from './usage.js';
|
|
10
10
|
import { askInteractiveChoice, closeRL, tty } from './interactive.js';
|
|
11
11
|
import { formatProfile, storedProfile, writePluginSettings } from './profile.js';
|
|
12
|
+
import { readDiagSchedule, setDiagSchedule } from './dashboard.js';
|
|
12
13
|
function applyFlags(base) {
|
|
13
14
|
const next = { ...base };
|
|
14
15
|
if (comboDefaultFlag !== undefined)
|
|
@@ -63,7 +64,7 @@ async function askProfile(current) {
|
|
|
63
64
|
if (ponytail.status !== 'selected')
|
|
64
65
|
return null;
|
|
65
66
|
next.ponytailDefault = ponytail.value;
|
|
66
|
-
const cur = await askInteractiveChoice('Display currency (usage/
|
|
67
|
+
const cur = await askInteractiveChoice('Display currency (usage/dashboard reports)', CURRENCY_CODES.map((v) => ({
|
|
67
68
|
value: v, label: v,
|
|
68
69
|
})), next.currency);
|
|
69
70
|
if (cur.status !== 'selected')
|
|
@@ -71,9 +72,97 @@ async function askProfile(current) {
|
|
|
71
72
|
next.currency = cur.value;
|
|
72
73
|
return next;
|
|
73
74
|
}
|
|
75
|
+
async function askDiagSchedule(current) {
|
|
76
|
+
const picked = await askInteractiveChoice('Diagnosis auto-check', [
|
|
77
|
+
{ value: 'manual', label: 'manual', hint: 'check on open' },
|
|
78
|
+
{ value: 'daily', label: 'daily' },
|
|
79
|
+
{ value: 'weekly', label: 'weekly' },
|
|
80
|
+
{ value: 'monthly', label: 'monthly' },
|
|
81
|
+
], current);
|
|
82
|
+
if (picked.status !== 'selected')
|
|
83
|
+
return null;
|
|
84
|
+
return picked.value;
|
|
85
|
+
}
|
|
86
|
+
const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis'];
|
|
87
|
+
function settingsUsage() {
|
|
88
|
+
console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis] [--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] [--diag-schedule manual|daily|weekly|monthly] [--dry-run]');
|
|
89
|
+
}
|
|
90
|
+
// Single-setting jump: `tersio settings diagnosis` prompts only that value
|
|
91
|
+
// instead of walking the whole chain. Returns false when aborted.
|
|
92
|
+
async function runSingleSetting(name, current, nextDiag) {
|
|
93
|
+
const next = { ...current };
|
|
94
|
+
let diag = nextDiag;
|
|
95
|
+
switch (name) {
|
|
96
|
+
case 'combo': {
|
|
97
|
+
const combo = await askInteractiveChoice('Session-start defaults — Combo preset', [
|
|
98
|
+
{ value: 'off', label: 'off' },
|
|
99
|
+
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
100
|
+
{ value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
|
|
101
|
+
{ value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
102
|
+
], current.comboDefault);
|
|
103
|
+
if (combo.status !== 'selected')
|
|
104
|
+
return null;
|
|
105
|
+
const preset = COMBO_PRESET_MODES[combo.value] ?? COMBO_PRESET_MODES.off;
|
|
106
|
+
next.comboDefault = combo.value;
|
|
107
|
+
next.cavemanDefault = preset.caveman;
|
|
108
|
+
next.rtkDefault = preset.rtk;
|
|
109
|
+
next.ponytailDefault = preset.ponytail;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'caveman': {
|
|
113
|
+
const picked = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
|
|
114
|
+
value: v, label: v,
|
|
115
|
+
})), next.cavemanDefault);
|
|
116
|
+
if (picked.status !== 'selected')
|
|
117
|
+
return null;
|
|
118
|
+
next.cavemanDefault = picked.value;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'rtk': {
|
|
122
|
+
const picked = await askInteractiveChoice('RTK default', [
|
|
123
|
+
{ value: 'on', label: 'on' },
|
|
124
|
+
{ value: 'off', label: 'off' },
|
|
125
|
+
], next.rtkDefault ? 'on' : 'off');
|
|
126
|
+
if (picked.status !== 'selected')
|
|
127
|
+
return null;
|
|
128
|
+
next.rtkDefault = picked.value === 'on';
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'ponytail': {
|
|
132
|
+
const picked = await askInteractiveChoice('Ponytail default', [...PONYTAIL_DEFAULTS].map((v) => ({
|
|
133
|
+
value: v, label: v,
|
|
134
|
+
})), next.ponytailDefault);
|
|
135
|
+
if (picked.status !== 'selected')
|
|
136
|
+
return null;
|
|
137
|
+
next.ponytailDefault = picked.value;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'currency': {
|
|
141
|
+
const picked = await askInteractiveChoice('Display currency (usage/dashboard reports)', CURRENCY_CODES.map((v) => ({
|
|
142
|
+
value: v, label: v,
|
|
143
|
+
})), next.currency);
|
|
144
|
+
if (picked.status !== 'selected')
|
|
145
|
+
return null;
|
|
146
|
+
next.currency = picked.value;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'diagnosis': {
|
|
150
|
+
const picked = await askDiagSchedule(diag);
|
|
151
|
+
if (picked === null)
|
|
152
|
+
return null;
|
|
153
|
+
diag = picked;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
default: {
|
|
157
|
+
console.error(`[fail] Invalid setting: ${name}. Valid: ${SETTING_NAMES.join(', ')}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { profile: next, diag };
|
|
162
|
+
}
|
|
74
163
|
function printSettingsTable(current) {
|
|
75
164
|
console.log('\n=== Tersio Settings ===');
|
|
76
|
-
// Currency lives here no longer: the
|
|
165
|
+
// Currency lives here no longer: the dashboard owns displaying and
|
|
77
166
|
// persisting it (its picker POSTs to /currency); settings can still set
|
|
78
167
|
// the stored default via --currency or the prompt below.
|
|
79
168
|
const rows = [
|
|
@@ -81,6 +170,7 @@ function printSettingsTable(current) {
|
|
|
81
170
|
['caveman', current.cavemanDefault, [...CAVEMAN_DEFAULTS].join(' | ')],
|
|
82
171
|
['rtk', current.rtkDefault ? 'on' : 'off', 'on | off'],
|
|
83
172
|
['ponytail', current.ponytailDefault, [...PONYTAIL_DEFAULTS].join(' | ')],
|
|
173
|
+
['diagnosis', readDiagSchedule(), 'manual | daily | weekly | monthly'],
|
|
84
174
|
];
|
|
85
175
|
for (const l of textTable(['Setting', 'Current', 'Valid values'], rows, [false, false, false], 64)) {
|
|
86
176
|
console.log(l);
|
|
@@ -91,7 +181,32 @@ async function runSettings() {
|
|
|
91
181
|
const current = await storedProfile();
|
|
92
182
|
printSettingsTable(current);
|
|
93
183
|
let next;
|
|
94
|
-
|
|
184
|
+
let nextDiag = readDiagSchedule();
|
|
185
|
+
if (diagScheduleFlag !== undefined)
|
|
186
|
+
nextDiag = diagScheduleFlag;
|
|
187
|
+
if (settingArg !== null) {
|
|
188
|
+
if (!SETTING_NAMES.includes(settingArg)) {
|
|
189
|
+
console.error(`[fail] Invalid setting: ${settingArg}. Valid: ${SETTING_NAMES.join(', ')}`);
|
|
190
|
+
closeRL();
|
|
191
|
+
process.exit(1);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!tty()) {
|
|
195
|
+
console.log('\n Single-setting jump needs a terminal.');
|
|
196
|
+
settingsUsage();
|
|
197
|
+
closeRL();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const single = await runSingleSetting(settingArg, current, nextDiag);
|
|
201
|
+
if (single === null) {
|
|
202
|
+
closeRL();
|
|
203
|
+
process.exit(130);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
next = single.profile;
|
|
207
|
+
nextDiag = single.diag;
|
|
208
|
+
}
|
|
209
|
+
else if (profileFlagsGiven || currencyGiven || diagScheduleFlag !== undefined) {
|
|
95
210
|
next = applyFlags(current);
|
|
96
211
|
}
|
|
97
212
|
else if (tty()) {
|
|
@@ -101,22 +216,30 @@ async function runSettings() {
|
|
|
101
216
|
process.exit(130);
|
|
102
217
|
return;
|
|
103
218
|
}
|
|
219
|
+
const picked = await askDiagSchedule(nextDiag);
|
|
220
|
+
if (picked === null) {
|
|
221
|
+
closeRL();
|
|
222
|
+
process.exit(130);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
nextDiag = picked;
|
|
104
226
|
}
|
|
105
227
|
else {
|
|
106
228
|
console.log('\n No flags given and no TTY — nothing to change.');
|
|
107
|
-
|
|
229
|
+
settingsUsage();
|
|
108
230
|
closeRL();
|
|
109
231
|
return;
|
|
110
232
|
}
|
|
111
233
|
if (dryRun) {
|
|
112
|
-
console.log(`\n [dry-run] would set defaults: ${formatProfile(next)}`);
|
|
234
|
+
console.log(`\n [dry-run] would set defaults: ${formatProfile(next)} · diagnosis=${nextDiag}`);
|
|
113
235
|
closeRL();
|
|
114
236
|
return;
|
|
115
237
|
}
|
|
116
238
|
await writePluginSettings(next, {});
|
|
117
|
-
|
|
239
|
+
setDiagSchedule(nextDiag);
|
|
240
|
+
console.log(` Defaults: ${formatProfile(next)} · diagnosis=${nextDiag}`);
|
|
118
241
|
console.log(' Applies to fresh sessions only — anything persisted with /combo, /caveman, or /rtk wins.');
|
|
119
|
-
console.log(' Currency applies to usage/
|
|
242
|
+
console.log(' Currency applies to usage/dashboard reports; --currency overrides it per run.');
|
|
120
243
|
closeRL();
|
|
121
244
|
}
|
|
122
245
|
export { runSettings };
|
package/cli/uninstall.js
CHANGED
|
@@ -56,8 +56,8 @@ async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
|
|
|
56
56
|
async function runUninstall(options = {}) {
|
|
57
57
|
const shouldDryRun = options.dryRun ?? dryRun;
|
|
58
58
|
const confirmed = (options.yes ?? yes) || shouldDryRun;
|
|
59
|
-
// Ponytail
|
|
60
|
-
// --keep-ponytail opts out and reinstall always preserves it.
|
|
59
|
+
// Ponytail is bundled with Tersio's presets, so a full uninstall removes
|
|
60
|
+
// its copy too; --keep-ponytail opts out and reinstall always preserves it.
|
|
61
61
|
const shouldRemovePonytail = options.removePonytail ?? (removePonytail || !keepPonytail);
|
|
62
62
|
const shouldRemoveRtk = options.removeRtk ?? removeRtk;
|
|
63
63
|
console.log('\n=== Tersio Uninstall ===\n');
|
|
@@ -135,8 +135,8 @@ async function runUninstall(options = {}) {
|
|
|
135
135
|
await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
// Remove the Ponytail
|
|
139
|
-
//
|
|
138
|
+
// Remove the bundled Ponytail copy (dep entry exists only on
|
|
139
|
+
// pre-bundle installs); the config.yml entry was filtered above.
|
|
140
140
|
if (shouldRemovePonytail) {
|
|
141
141
|
const pluginsPkgPath = path.join(pluginsDir, 'package.json');
|
|
142
142
|
await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
|
package/cli/update.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { BUN_BIN_DIR, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, dryRun, execP, parseJsonObject, verbose, } from './common.js';
|
|
6
6
|
import { execNetwork } from './interactive.js';
|
|
7
|
-
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
7
|
+
import { readTextIfExists, tersioDataPath } from '../extensions/lib/utils.js';
|
|
8
8
|
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
9
9
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
|
|
10
10
|
const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -39,7 +39,7 @@ async function latestPublishedVersion() {
|
|
|
39
39
|
// (cached under OMP_PLUGINS_DIR) unless force skips the cache — an explicit
|
|
40
40
|
// "check for updates" must never trust a stale cache.
|
|
41
41
|
async function checkForUpdate(force = false) {
|
|
42
|
-
const cachePath =
|
|
42
|
+
const cachePath = tersioDataPath('update-check.json', 'tersio-update-check.json');
|
|
43
43
|
const cached = parseJsonObject(await readTextIfExists(cachePath));
|
|
44
44
|
const cacheFresh = typeof cached?.lastCheck === 'number' && Date.now() - cached.lastCheck < UPDATE_CHECK_TTL_MS;
|
|
45
45
|
if (!force && cacheFresh && cached?.latest)
|
|
@@ -68,20 +68,6 @@ async function settle(work, ms) {
|
|
|
68
68
|
clearTimeout(timer);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
// Plain `npm view <spec> version` with no spinner, for background probes.
|
|
72
|
-
async function npmViewVersion(spec, timeoutMs) {
|
|
73
|
-
try {
|
|
74
|
-
const exe = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
75
|
-
const args = IS_WINDOWS
|
|
76
|
-
? ['/d', '/s', '/c', 'npm', 'view', spec, 'version', '--prefer-online']
|
|
77
|
-
: ['view', spec, 'version', '--prefer-online'];
|
|
78
|
-
const r = await execP(exe, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024, windowsHide: true, shell: false });
|
|
79
|
-
return r.stdout.trim() || null;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
71
|
// Run a child with inherited stdio for live output. Used when the child
|
|
86
72
|
// renders its own interactive UI (the delegated installer runs Clack
|
|
87
73
|
// spinners), which a concurrent outer spinner would corrupt into stray bars.
|
|
@@ -100,20 +86,19 @@ async function probeUpdatePlan(cliLatest) {
|
|
|
100
86
|
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
101
87
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
102
88
|
const ruleDest = path.join(OMP_AGENT_DIR, 'extensions', 'caveman-session', 'rule.md');
|
|
103
|
-
const [rtkLocal, rtkRelease, ruleLocalText, ruleRemoteText, ponytailLocalText
|
|
89
|
+
const [rtkLocal, rtkRelease, ruleLocalText, ruleRemoteText, ponytailLocalText] = await Promise.all([
|
|
104
90
|
settle(execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null), 6000),
|
|
105
91
|
settle(fetchJson(RTK_RELEASE_API).then((r) => normalizeRtkVersion(r.tag_name) || null), 4000),
|
|
106
92
|
readTextIfExists(ruleDest),
|
|
107
93
|
settle(httpsGet(CAVEMAN_REMOTE_RULE), 4000),
|
|
108
94
|
readTextIfExists(ponytailPkg),
|
|
109
|
-
settle(npmViewVersion('@dietrichgebert/ponytail', 4000), 5000),
|
|
110
95
|
]);
|
|
111
96
|
const shortHash = (text) => text === null ? null : sha256Hex(text).slice(0, 8);
|
|
112
97
|
return {
|
|
113
98
|
cli: cliLatest,
|
|
114
99
|
rtk: [rtkLocal, rtkRelease],
|
|
115
100
|
rule: [shortHash(ruleLocalText), shortHash(ruleRemoteText)],
|
|
116
|
-
ponytail:
|
|
101
|
+
ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
|
|
117
102
|
};
|
|
118
103
|
}
|
|
119
104
|
// Every add-on prints its status: version jump when stale, "up to date"
|
|
@@ -136,7 +121,12 @@ function planLines(plan) {
|
|
|
136
121
|
push('Tersio', PACKAGE_VERSION, plan.cli);
|
|
137
122
|
push('RTK', plan.rtk[0], plan.rtk[1]);
|
|
138
123
|
push('Caveman rule', plan.rule[0], plan.rule[1]);
|
|
139
|
-
|
|
124
|
+
if (plan.ponytail)
|
|
125
|
+
status.push(`Ponytail: ${plan.ponytail} (bundled with tersio)`);
|
|
126
|
+
else {
|
|
127
|
+
status.push('Ponytail: missing → bundled reinstall');
|
|
128
|
+
stale.push('Ponytail: missing → bundled reinstall');
|
|
129
|
+
}
|
|
140
130
|
return { stale, status };
|
|
141
131
|
}
|
|
142
132
|
async function runLatestUpdate() {
|
package/cli/usage.js
CHANGED
|
@@ -132,7 +132,7 @@ function printReport(report) {
|
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
const t = report.tokens;
|
|
135
|
-
console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read
|
|
135
|
+
console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`);
|
|
136
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.)`);
|
|
137
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);
|
|
138
138
|
if (models.length) {
|