@krtclcdy/tersio 2.24.0 → 2.25.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 +28 -24
- package/cli/banner.js +7 -5
- package/cli/common.js +14 -13
- package/cli/currency.js +2 -7
- package/cli/dashboard.js +220 -13
- package/cli/doctor-fix.js +99 -29
- package/cli/doctor.js +32 -35
- package/cli/hosts.js +23 -26
- package/cli/install.js +228 -115
- package/cli/interactive.js +65 -21
- package/cli/manifest.js +8 -4
- package/cli/profile.js +49 -16
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +29 -32
- package/cli/settings.js +40 -16
- package/cli/uninstall.js +79 -29
- package/cli/update.js +27 -20
- package/cli/usage.js +54 -19
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +75 -20
- package/extensions/ai-addons-updater/index.ts +67 -23
- package/extensions/caveman-session/index.js +33 -75
- package/extensions/caveman-session/index.ts +35 -77
- package/extensions/combo-toggle/index.js +40 -62
- package/extensions/combo-toggle/index.ts +38 -72
- package/extensions/lib/utils.js +64 -15
- package/extensions/lib/utils.ts +62 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +38 -57
- package/extensions/rtk-session/index.ts +36 -55
- package/extensions/shared/carbon.js +4 -10
- package/extensions/shared/carbon.ts +5 -10
- package/extensions/shared/host.js +95 -56
- package/extensions/shared/host.ts +106 -60
- package/extensions/shared/plugin-settings.js +6 -18
- package/extensions/shared/plugin-settings.ts +6 -18
- package/extensions/shared/pricing.js +79 -27
- package/extensions/shared/pricing.ts +80 -28
- package/extensions/shared/rtk-gain.js +4 -11
- package/extensions/shared/rtk-gain.ts +4 -11
- package/extensions/shared/session-state.js +28 -39
- package/extensions/shared/session-state.ts +29 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +312 -57
- package/extensions/shared/usage-ledger.ts +321 -61
- package/extensions/shared/usage-store.js +354 -38
- package/extensions/shared/usage-store.ts +360 -43
- package/extensions/tersio-commands/index.js +5 -14
- package/extensions/tersio-commands/index.ts +5 -14
- package/package.json +12 -7
- package/tersio.js +4 -6
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -24
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
package/cli/uninstall.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// cli/uninstall.ts — remove managed extensions, plugins, and binaries.
|
|
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, PACKAGE_NAME, RTK_BINARY_NAME, args, dryRun, keepPonytail, removePonytail, removeRtk, yes, debug, writeConfigLines, } from './common.js';
|
|
5
|
-
import { askInteractiveChoice, closeRL, confirmDestructive, tty } from './interactive.js';
|
|
4
|
+
import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, RTK_BINARY_NAME, args, dryRun, keepPonytail, removePonytail, removeRtk, yes, debug, writeConfigLines, } from './common.js';
|
|
5
|
+
import { askInteractiveChoice, closeRL, confirmDestructive, tty, sayTagged } from './interactive.js';
|
|
6
6
|
import { detectHosts, hostHint, hostLabel, parseHostArg } from './hosts.js';
|
|
7
7
|
import { findHoistedPackage, piAgentDir, readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
10
|
-
// The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a
|
|
11
|
-
// shared binary are not ours to delete.
|
|
10
|
+
// The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a shared binary are not ours to delete.
|
|
12
11
|
const PONYTAIL_PKG = '@dietrichgebert/ponytail';
|
|
13
12
|
const PI_TREE_DIRS = [
|
|
14
13
|
'caveman-session',
|
|
@@ -18,11 +17,12 @@ const PI_TREE_DIRS = [
|
|
|
18
17
|
'tersio-commands',
|
|
19
18
|
'shared',
|
|
20
19
|
'lib',
|
|
20
|
+
'opencode',
|
|
21
21
|
];
|
|
22
22
|
// One `pi remove`, for both packages the installer adds there.
|
|
23
23
|
async function piRemove(spec, shouldDryRun) {
|
|
24
24
|
if (shouldDryRun) {
|
|
25
|
-
|
|
25
|
+
sayTagged(` [dry-run] would run: pi remove ${spec}`);
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
try {
|
|
@@ -35,11 +35,11 @@ async function piRemove(spec, shouldDryRun) {
|
|
|
35
35
|
resolve();
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
sayTagged(` [ok] removed the pi package ${spec}`);
|
|
39
39
|
}
|
|
40
40
|
catch (e) {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
sayTagged(` [fail] pi remove: ${e.message}`);
|
|
42
|
+
sayTagged(` [hint] Manual: pi remove ${spec}`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
// Only the skill directories the installer copied, never pi's own skills dir.
|
|
@@ -52,13 +52,24 @@ async function removeCopiedPonytailSkills(shouldDryRun) {
|
|
|
52
52
|
await removeUninstallTarget(path.join(piAgentDir(), 'skills', entry.name), shouldDryRun);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
// pi
|
|
56
|
-
|
|
57
|
-
// route, so neither host keeps a package we installed on its behalf.
|
|
58
|
-
async function removePiLayer(host, shouldDryRun, shouldRemovePonytail) {
|
|
55
|
+
// pi removal delegates to pi remove; always list and confirm first.
|
|
56
|
+
async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRemoveRtk, confirmed) {
|
|
59
57
|
const targets = PI_TREE_DIRS.map((dir) => path.join(piAgentDir(), 'extensions', dir));
|
|
58
|
+
const rtkWiring = path.join(piAgentDir(), 'extensions', 'rtk.ts');
|
|
60
59
|
if (!host.installed && !host.declared) {
|
|
61
|
-
|
|
60
|
+
sayTagged(` [skip] ${host.label} — nothing installed (${host.installCmd})`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
console.log('Will remove:');
|
|
64
|
+
for (const t of targets)
|
|
65
|
+
console.log(` ${t}`);
|
|
66
|
+
if (shouldRemovePonytail)
|
|
67
|
+
console.log(` npm:${PONYTAIL_PKG} (pi package)`);
|
|
68
|
+
if (shouldRemoveRtk)
|
|
69
|
+
console.log(` ${rtkWiring} (rtk wiring)`);
|
|
70
|
+
console.log(` ${tersioSettingsFile()} (session defaults)`);
|
|
71
|
+
if (!confirmed && !(await confirmDestructive(`Remove Tersio from ${host.label}?`))) {
|
|
72
|
+
closeRL();
|
|
62
73
|
return false;
|
|
63
74
|
}
|
|
64
75
|
if (host.declared)
|
|
@@ -68,6 +79,11 @@ async function removePiLayer(host, shouldDryRun, shouldRemovePonytail) {
|
|
|
68
79
|
await removeCopiedPonytailSkills(shouldDryRun);
|
|
69
80
|
}
|
|
70
81
|
await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
|
|
82
|
+
// rtk's wiring is a loose file, not one of the tree dirs, so it survived --remove-rtk on pi while OMP removed its copy.
|
|
83
|
+
if (shouldRemoveRtk) {
|
|
84
|
+
await removeUninstallTarget(rtkWiring, shouldDryRun, false);
|
|
85
|
+
await removeUninstallTarget(path.join(BUN_BIN_DIR, RTK_BINARY_NAME), shouldDryRun, false);
|
|
86
|
+
}
|
|
71
87
|
await clearSessionDefaults(shouldDryRun);
|
|
72
88
|
return true;
|
|
73
89
|
}
|
|
@@ -91,11 +107,11 @@ async function updateJsonFile(filePath, mutate, dryRunNote, writeNote, dryRun) {
|
|
|
91
107
|
if (!mutate(data))
|
|
92
108
|
return;
|
|
93
109
|
if (dryRun) {
|
|
94
|
-
|
|
110
|
+
sayTagged(` [dry-run] ${dryRunNote}`);
|
|
95
111
|
return;
|
|
96
112
|
}
|
|
97
113
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
98
|
-
|
|
114
|
+
sayTagged(` [write] ${writeNote}`);
|
|
99
115
|
}
|
|
100
116
|
function dropKey(section, key) {
|
|
101
117
|
if (!section || typeof section !== 'object')
|
|
@@ -109,14 +125,14 @@ function dropKey(section, key) {
|
|
|
109
125
|
async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
|
|
110
126
|
try {
|
|
111
127
|
if (shouldDryRun) {
|
|
112
|
-
|
|
128
|
+
sayTagged(` [dry-run] would remove ${target}`);
|
|
113
129
|
return;
|
|
114
130
|
}
|
|
115
131
|
if (recursive)
|
|
116
132
|
await fs.rm(target, { recursive: true, force: true });
|
|
117
133
|
else
|
|
118
134
|
await fs.unlink(target);
|
|
119
|
-
|
|
135
|
+
sayTagged(` [rm] ${target}`);
|
|
120
136
|
}
|
|
121
137
|
catch {
|
|
122
138
|
debug(`Could not remove ${target}`);
|
|
@@ -163,14 +179,45 @@ async function runUninstall(options = {}) {
|
|
|
163
179
|
}
|
|
164
180
|
const selected = detectHosts().find((h) => h.id === host);
|
|
165
181
|
if (host === 'pi') {
|
|
166
|
-
const removed = await removePiLayer(selected, shouldDryRun, shouldRemovePonytail);
|
|
182
|
+
const removed = await removePiLayer(selected, shouldDryRun, shouldRemovePonytail, shouldRemoveRtk, confirmed);
|
|
167
183
|
if (removed)
|
|
168
|
-
console.log('\nDone. Restart pi
|
|
184
|
+
console.log('\nDone. Restart pi, then /combo medium.');
|
|
169
185
|
closeRL();
|
|
170
186
|
return removed;
|
|
171
187
|
}
|
|
188
|
+
if (host === 'opencode') {
|
|
189
|
+
const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
190
|
+
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
191
|
+
if (!selected.installed && !selected.declared) {
|
|
192
|
+
sayTagged(` [skip] ${selected.label} — nothing installed (${selected.installCmd})`);
|
|
193
|
+
closeRL();
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
console.log('Will remove:');
|
|
197
|
+
console.log(` ${dir}`);
|
|
198
|
+
console.log(` the tersio entry in ${configPath}`);
|
|
199
|
+
if (!confirmed && !(await confirmDestructive('Remove these Tersio files?'))) {
|
|
200
|
+
closeRL();
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
await removeUninstallTarget(dir, shouldDryRun);
|
|
204
|
+
if (!shouldDryRun) {
|
|
205
|
+
try {
|
|
206
|
+
const raw = await readTextIfExists(configPath);
|
|
207
|
+
const config = raw ? JSON.parse(raw) : {};
|
|
208
|
+
if (Array.isArray(config.plugins)) {
|
|
209
|
+
config.plugins = config.plugins.filter((p) => typeof p !== 'string' || !p.includes('/opencode/plugins/tersio/'));
|
|
210
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch { /* config missing or not JSON; nothing to scrub */ }
|
|
214
|
+
}
|
|
215
|
+
console.log('\nDone. Restart OpenCode to drop the plugin.');
|
|
216
|
+
closeRL();
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
172
219
|
if (!selected.installed)
|
|
173
|
-
|
|
220
|
+
sayTagged(` [note] ${selected.label} has no tersio install; removing what is left behind.`);
|
|
174
221
|
const extDir = path.join(OMP_AGENT_DIR, 'extensions');
|
|
175
222
|
const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
|
|
176
223
|
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
@@ -187,6 +234,8 @@ async function runUninstall(options = {}) {
|
|
|
187
234
|
'lib',
|
|
188
235
|
// Legacy: imports session-state.js, so it warns once that module is gone.
|
|
189
236
|
'aaa-combo-boot',
|
|
237
|
+
// Foreign to the OMP loader; never shipped here on purpose.
|
|
238
|
+
'opencode',
|
|
190
239
|
].map((dir) => path.join(extDir, dir));
|
|
191
240
|
console.log('Will remove:');
|
|
192
241
|
for (const t of targets) {
|
|
@@ -213,8 +262,7 @@ async function runUninstall(options = {}) {
|
|
|
213
262
|
return false;
|
|
214
263
|
if (shouldRemovePonytail && l.includes('ponytail') && l.includes('pi-extension'))
|
|
215
264
|
return false;
|
|
216
|
-
// Our rtk wiring, absolute or relative. Leaving it listed would keep
|
|
217
|
-
// OMP loading a hook for a package that is no longer installed.
|
|
265
|
+
// Our rtk wiring, absolute or relative. Leaving it listed would keep OMP loading a hook for a package that is no longer installed.
|
|
218
266
|
const entry = l.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '');
|
|
219
267
|
if (entry.endsWith(`extensions${path.sep}rtk.ts`) || entry.endsWith('extensions/rtk.ts'))
|
|
220
268
|
return false;
|
|
@@ -222,23 +270,27 @@ async function runUninstall(options = {}) {
|
|
|
222
270
|
});
|
|
223
271
|
if (lines.length !== before) {
|
|
224
272
|
if (shouldDryRun)
|
|
225
|
-
|
|
273
|
+
sayTagged(` [dry-run] would remove ${before - lines.length} config.yml entries`);
|
|
226
274
|
else
|
|
227
275
|
await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
|
|
228
276
|
}
|
|
229
277
|
}
|
|
278
|
+
// Cleared last: writeConfigLines above would just make a fresh one.
|
|
279
|
+
for (const backup of [`${configPath}.bak`, path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'extensions', 'caveman-session', 'rule.md.bak')]) {
|
|
280
|
+
await removeUninstallTarget(backup, shouldDryRun, false);
|
|
281
|
+
}
|
|
230
282
|
// Remove the bundled Ponytail copy; its config.yml entry is already filtered.
|
|
231
283
|
if (shouldRemovePonytail) {
|
|
232
284
|
const pluginsPkgPath = path.join(pluginsDir, 'package.json');
|
|
233
285
|
await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
|
|
234
286
|
try {
|
|
235
287
|
if (shouldDryRun)
|
|
236
|
-
|
|
288
|
+
sayTagged(` [dry-run] would remove ${ponytailPkgDir}`);
|
|
237
289
|
else {
|
|
238
290
|
await fs.rm(ponytailPkgDir, { recursive: true, force: true });
|
|
239
|
-
|
|
291
|
+
sayTagged(` [rm] ${ponytailPkgDir}`);
|
|
240
292
|
await fs.rm(path.dirname(ponytailPkgDir));
|
|
241
|
-
|
|
293
|
+
sayTagged(` [rm] ${path.dirname(ponytailPkgDir)} (empty scope)`);
|
|
242
294
|
}
|
|
243
295
|
}
|
|
244
296
|
catch {
|
|
@@ -254,9 +306,7 @@ async function runUninstall(options = {}) {
|
|
|
254
306
|
if ((await readTextIfExists(path.join(selfPluginDir, 'package.json'))) !== null) {
|
|
255
307
|
await removeUninstallTarget(selfPluginDir, shouldDryRun);
|
|
256
308
|
}
|
|
257
|
-
// Our rtk wiring goes
|
|
258
|
-
// rewriting bash calls to a package this uninstall just removed. The binary
|
|
259
|
-
// stays unless --remove-rtk, since it is a shared tool on PATH.
|
|
309
|
+
// Our rtk wiring goes too, or OMP keeps rewriting for a removed package.
|
|
260
310
|
await removeUninstallTarget(path.join(extDir, 'rtk.ts'), shouldDryRun);
|
|
261
311
|
if (shouldRemoveRtk)
|
|
262
312
|
await removeUninstallTarget(rtkBin, shouldDryRun, false);
|
package/cli/update.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, dryRun, execP, parseJsonObject, verbose, } from './common.js';
|
|
6
|
-
import { execNetwork } from './interactive.js';
|
|
5
|
+
import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, execP, parseJsonObject, verbose, } from './common.js';
|
|
6
|
+
import { execNetwork, sayTagged } from './interactive.js';
|
|
7
7
|
import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/lib/utils.js';
|
|
8
8
|
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
9
9
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
|
|
10
|
+
import { parseHostArg } from './hosts.js';
|
|
10
11
|
const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
11
12
|
function newerThan(a, b) {
|
|
12
13
|
const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
|
|
@@ -34,8 +35,7 @@ async function latestPublishedVersion() {
|
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
// A newer published version, null when up to date, 'unknown' when the registry
|
|
38
|
-
// is unreachable. Cached for a TTL unless force, which must not trust a cache.
|
|
38
|
+
// A newer published version, null when up to date, 'unknown' when the registry is unreachable. Cached for a TTL unless force, which must not trust a cache.
|
|
39
39
|
async function checkForUpdate(force = false) {
|
|
40
40
|
const cachePath = tersioDataPath('update-check.json', 'tersio-update-check.json');
|
|
41
41
|
const cached = parseJsonObject(await readTextIfExists(cachePath));
|
|
@@ -48,8 +48,7 @@ async function checkForUpdate(force = false) {
|
|
|
48
48
|
await fs.writeFile(cachePath, JSON.stringify({ latest, lastCheck: Date.now() }) + '\n', 'utf8').catch(() => { });
|
|
49
49
|
return newerThan(latest, PACKAGE_VERSION) ? latest : null;
|
|
50
50
|
}
|
|
51
|
-
// Unreachable registry: a stale cache naming a newer release is still
|
|
52
|
-
// actionable, else report unknown so nobody claims "latest".
|
|
51
|
+
// Unreachable registry: a stale cache naming a newer release is still actionable, else report unknown so nobody claims "latest".
|
|
53
52
|
return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : 'unknown';
|
|
54
53
|
}
|
|
55
54
|
// Race a probe against a timeout; failures resolve null so nothing blocks.
|
|
@@ -75,8 +74,7 @@ async function execInherit(cmd, args) {
|
|
|
75
74
|
if (code !== 0)
|
|
76
75
|
throw new Error(`${cmd} exited with code ${code}`);
|
|
77
76
|
}
|
|
78
|
-
// Current → latest per add-on. Every probe is capped and nullable, and the
|
|
79
|
-
// plan prints `unknown` for anything unreachable.
|
|
77
|
+
// Current → latest per add-on. Every probe is capped and nullable, and the plan prints `unknown` for anything unreachable.
|
|
80
78
|
async function probeUpdatePlan(cliLatest) {
|
|
81
79
|
const rtkBin = resolveRtkBinary();
|
|
82
80
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
@@ -97,8 +95,7 @@ async function probeUpdatePlan(cliLatest) {
|
|
|
97
95
|
ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
|
|
98
96
|
};
|
|
99
97
|
}
|
|
100
|
-
// Every add-on prints a version jump when stale, "up to date" otherwise, and
|
|
101
|
-
// "unknown" when its source cannot be reached.
|
|
98
|
+
// Every add-on prints a version jump when stale, "up to date" otherwise, and "unknown" when its source cannot be reached.
|
|
102
99
|
function planLines(plan) {
|
|
103
100
|
const stale = [];
|
|
104
101
|
const status = [];
|
|
@@ -131,6 +128,18 @@ async function runLatestUpdate() {
|
|
|
131
128
|
forwardedArgs.push('--dry-run');
|
|
132
129
|
if (verbose)
|
|
133
130
|
forwardedArgs.push('--verbose');
|
|
131
|
+
// A pinned host refreshes that host's tree; bare update keeps the OMP default.
|
|
132
|
+
let pinned;
|
|
133
|
+
try {
|
|
134
|
+
pinned = parseHostArg(args);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
console.error(`[fail] ${e.message}`);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (pinned)
|
|
142
|
+
forwardedArgs.push('--host', pinned);
|
|
134
143
|
const npmCommand = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
135
144
|
// `--prefer-online` beats the local cache, so a stale @latest cannot pin down.
|
|
136
145
|
const cliLatest = await latestPublishedVersion();
|
|
@@ -140,8 +149,8 @@ async function runLatestUpdate() {
|
|
|
140
149
|
const { stale, status } = planLines(plan);
|
|
141
150
|
if (dryRun) {
|
|
142
151
|
console.log(`tersio update (dry-run):\n ${status.join('\n ')}`);
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
sayTagged(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
153
|
+
sayTagged(` [dry-run] would delegate: npm exec --yes --prefer-online --package=${PACKAGE_NAME}${target} -- tersio --apply-update ${forwardedArgs.join(' ')}`);
|
|
145
154
|
return;
|
|
146
155
|
}
|
|
147
156
|
console.log(`Checking for updates:\n ${status.join('\n ')}`);
|
|
@@ -169,8 +178,7 @@ async function runLatestUpdate() {
|
|
|
169
178
|
process.exitCode = 1;
|
|
170
179
|
return;
|
|
171
180
|
}
|
|
172
|
-
// The npx delegation below only refreshes the OMP-side files; the globally
|
|
173
|
-
// installed CLI keeps its old version until npm -g runs. Refresh both.
|
|
181
|
+
// The npx delegation below only refreshes the OMP-side files; the globally installed CLI keeps its old version until npm -g runs. Refresh both.
|
|
174
182
|
const globalArgs = IS_WINDOWS
|
|
175
183
|
? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online']
|
|
176
184
|
: ['install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online'];
|
|
@@ -186,8 +194,8 @@ async function runLatestUpdate() {
|
|
|
186
194
|
process.stderr.write(globalResult.stderr);
|
|
187
195
|
}
|
|
188
196
|
catch (e) {
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
sayTagged(` [warn] Global CLI update skipped: ${e.message}`);
|
|
198
|
+
sayTagged(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
191
199
|
}
|
|
192
200
|
const npmArgs = [
|
|
193
201
|
'exec',
|
|
@@ -200,12 +208,11 @@ async function runLatestUpdate() {
|
|
|
200
208
|
...forwardedArgs,
|
|
201
209
|
];
|
|
202
210
|
const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
|
|
203
|
-
// Inherited stdio, no outer spinner
|
|
204
|
-
// (banner and per-file writes suppressed); this parent owns both the plan
|
|
205
|
-
// line above and the closing summary below — one voice, no repeats.
|
|
211
|
+
// Inherited stdio, no outer spinner; the delegated installer runs quiet and this parent owns both the plan line and the closing summary.
|
|
206
212
|
try {
|
|
207
213
|
await execInherit(npmCommand, npmCommandArgs);
|
|
208
|
-
|
|
214
|
+
const hostName = pinned === 'pi' ? 'pi' : pinned === 'opencode' ? 'OpenCode' : 'OMP';
|
|
215
|
+
console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart ${hostName}.`);
|
|
209
216
|
if (!dryRun) {
|
|
210
217
|
try {
|
|
211
218
|
await refreshPrices();
|
package/cli/usage.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
-
import { co2GramsFor, displayModelId, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readRtkAdoption, readRtkRecallDiagnostics, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
2
|
+
import { co2GramsFor, displayModelId, energyWhFor, foldModelsByLabel, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readRtkAdoption, readRtkRecallDiagnostics, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
3
|
import { resolveRtkBinary } from '../extensions/lib/utils.js';
|
|
4
4
|
import { readRtkGain } from '../extensions/shared/rtk-gain.js';
|
|
5
|
-
import { readUsageDb, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
5
|
+
import { readUsageDb, reparseGuardReport, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
6
6
|
import { withInteractiveSpinner } from './interactive.js';
|
|
7
7
|
import { PACKAGE_VERSION, currency } from './common.js';
|
|
8
8
|
import { formatCurrency } from './currency.js';
|
|
9
|
-
// RTK's history is machine-wide; rows group by command alone. 2000 groups
|
|
10
|
-
// covers a long history while still bounding /data.json.
|
|
9
|
+
// RTK's history is machine-wide; rows group by command alone. 2000 groups covers a long history while still bounding /data.json.
|
|
11
10
|
const RTK_COMMAND_ROWS = 2000;
|
|
12
11
|
export function summarizeUsage(rows) {
|
|
13
12
|
refreshPricesIfStale();
|
|
@@ -18,8 +17,7 @@ export function summarizeUsage(rows) {
|
|
|
18
17
|
detailCounts[r.detail] = (detailCounts[r.detail] ?? 0) + 1;
|
|
19
18
|
}
|
|
20
19
|
const byDetail = Object.entries(detailCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
21
|
-
// usage.db is a cache of the same per-message rows: sync best-effort, read
|
|
22
|
-
// stored, fall back to the live parse when the store is missing or stale.
|
|
20
|
+
// usage.db is a cache of the same per-message rows: sync best-effort, read stored, fall back to the live parse when the store is missing or stale.
|
|
23
21
|
let synced = false;
|
|
24
22
|
try {
|
|
25
23
|
synced = syncUsageDb();
|
|
@@ -27,6 +25,9 @@ export function summarizeUsage(rows) {
|
|
|
27
25
|
catch {
|
|
28
26
|
synced = false;
|
|
29
27
|
}
|
|
28
|
+
const guard = reparseGuardReport();
|
|
29
|
+
if (guard)
|
|
30
|
+
console.warn(`[warn] usage mirror: ${guard}`);
|
|
30
31
|
let stored = null;
|
|
31
32
|
try {
|
|
32
33
|
stored = readUsageDb()?.tokens ?? null;
|
|
@@ -42,19 +43,43 @@ export function summarizeUsage(rows) {
|
|
|
42
43
|
let savedUsd = 0;
|
|
43
44
|
const byModelUsd = {};
|
|
44
45
|
const byModelBucketUsd = {};
|
|
46
|
+
const byHost = {};
|
|
47
|
+
const byDayModel = {};
|
|
45
48
|
let co2g = 0;
|
|
46
49
|
let energyWh = 0;
|
|
50
|
+
const addTok = (into, t) => {
|
|
51
|
+
into.input += t.input;
|
|
52
|
+
into.output += t.output;
|
|
53
|
+
into.cacheRead += t.cacheRead;
|
|
54
|
+
into.cacheWrite += t.cacheWrite;
|
|
55
|
+
};
|
|
56
|
+
// Price by exact key first (gateways can differ), then fold every map under the display label.
|
|
57
|
+
const folded = foldModelsByLabel(session.byModel, session.byModelMessages);
|
|
47
58
|
for (const [model, t] of Object.entries(session.byModel)) {
|
|
48
59
|
const c = usdCost(t, model);
|
|
49
60
|
usd += c.usd;
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const label = displayModelId(model);
|
|
62
|
+
byModelUsd[label] = (byModelUsd[label] ?? 0) + c.usd;
|
|
63
|
+
byModelBucketUsd[label] ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
64
|
+
addTok(byModelBucketUsd[label], c.buckets);
|
|
65
|
+
for (const [host, hb] of Object.entries(session.byHost[model] ?? {})) {
|
|
66
|
+
byHost[label] ??= {};
|
|
67
|
+
byHost[label][host] ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
68
|
+
addTok(byHost[label][host], hb);
|
|
69
|
+
}
|
|
52
70
|
co2g += co2GramsFor(model, t.output);
|
|
53
71
|
energyWh += energyWhFor(model, t.output);
|
|
54
72
|
if (!c.priced)
|
|
55
73
|
priced = false;
|
|
56
74
|
savedUsd += (t.cacheRead / 1e6) * priceFor(model).price.input;
|
|
57
75
|
}
|
|
76
|
+
for (const [day, per] of Object.entries(session.byDayModel)) {
|
|
77
|
+
for (const [model, n] of Object.entries(per)) {
|
|
78
|
+
const label = displayModelId(model);
|
|
79
|
+
byDayModel[day] ??= {};
|
|
80
|
+
byDayModel[day][label] = (byDayModel[day][label] ?? 0) + n;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
58
83
|
const byTool = Object.entries(session.byTool).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
59
84
|
return {
|
|
60
85
|
total: rows.length,
|
|
@@ -64,12 +89,13 @@ export function summarizeUsage(rows) {
|
|
|
64
89
|
empty: rows.length === 0 && session.messages === 0,
|
|
65
90
|
messages: session.messages,
|
|
66
91
|
tokens: session.totals,
|
|
67
|
-
byModel:
|
|
92
|
+
byModel: folded.byModel,
|
|
68
93
|
byModelUsd,
|
|
69
94
|
byModelBucketUsd,
|
|
70
|
-
byModelMessages:
|
|
95
|
+
byModelMessages: folded.byModelMessages,
|
|
96
|
+
byHost,
|
|
71
97
|
byDay: session.byDay,
|
|
72
|
-
byDayModel
|
|
98
|
+
byDayModel,
|
|
73
99
|
byTool,
|
|
74
100
|
recent: session.recent.map((r) => ({
|
|
75
101
|
...r,
|
|
@@ -113,11 +139,11 @@ function bar(frac, width = 12) {
|
|
|
113
139
|
const filled = Math.max(0, Math.min(width, Math.round(frac * width)));
|
|
114
140
|
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
115
141
|
}
|
|
142
|
+
const ROW_CAP = 15;
|
|
116
143
|
function pad(s, n) {
|
|
117
144
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
118
145
|
}
|
|
119
|
-
// Box-drawing table, plain text (piped-safe, byte-stable). Numeric columns
|
|
120
|
-
// right-align; long cells truncate with an ellipsis. Shared with cli/settings.ts.
|
|
146
|
+
// Box-drawing table, plain text (piped-safe, byte-stable). Numeric columns right-align; long cells truncate with an ellipsis. Shared with cli/settings.ts.
|
|
121
147
|
function textTable(headers, rows, right = [], maxW = 32) {
|
|
122
148
|
const cells = [headers, ...rows].map((r) => r.map((c) => (c.length > maxW ? c.slice(0, maxW - 1) + '…' : c)));
|
|
123
149
|
const widths = headers.map((_, i) => Math.max(...cells.map((r) => r[i].length)));
|
|
@@ -136,10 +162,15 @@ function printReport(report) {
|
|
|
136
162
|
return;
|
|
137
163
|
}
|
|
138
164
|
const t = report.tokens;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
165
|
+
const a = report.rtkAdoption;
|
|
166
|
+
for (const l of textTable(['Metric', 'Value'], [
|
|
167
|
+
['Tokens', `${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`],
|
|
168
|
+
['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.)`],
|
|
169
|
+
['RTK adoption', `${fmt(a.rtkCalls)}/${fmt(a.eligibleCalls)} eligible Bash calls use RTK (${a.adoptionPct.toFixed(1)}%) · ${fmt(a.missedCalls)} missed · recall ${report.rtkRecall.available ? `${report.rtkRecall.mode} (${report.rtkRecall.entries})` : 'unavailable'}`],
|
|
170
|
+
], [false, false], 118))
|
|
171
|
+
console.log(l);
|
|
172
|
+
const allModels = 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));
|
|
173
|
+
const models = allModels.slice(0, ROW_CAP);
|
|
143
174
|
if (models.length) {
|
|
144
175
|
console.log(' BY MODEL');
|
|
145
176
|
const top = models.reduce((m, [, b]) => Math.max(m, b.input + b.output + b.cacheRead + b.cacheWrite), 1);
|
|
@@ -147,11 +178,13 @@ function printReport(report) {
|
|
|
147
178
|
const mt = b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
148
179
|
const hit = b.input + b.cacheRead ? (b.cacheRead / (b.input + b.cacheRead)) * 100 : 0;
|
|
149
180
|
const usd = report.byModelUsd[model] ?? 0;
|
|
150
|
-
return [
|
|
181
|
+
return [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)}`];
|
|
151
182
|
});
|
|
152
183
|
for (const l of textTable(['Model', 'Input', 'Output', 'Cache r/w', report.currency, 'Hit', 'Share'], mrows, [false, true, true, true, true, true, false])) {
|
|
153
184
|
console.log(l);
|
|
154
185
|
}
|
|
186
|
+
if (allModels.length > ROW_CAP)
|
|
187
|
+
console.log(` showing top ${ROW_CAP} of ${fmt(allModels.length)} models — full set in the dashboard`);
|
|
155
188
|
}
|
|
156
189
|
const cmdRows = report.byTool.map(([tool, n]) => ({ name: tool, count: n, saved: null, avgPct: null, avgMs: null }));
|
|
157
190
|
for (const r of report.rtkGain.byCommand) {
|
|
@@ -165,7 +198,7 @@ function printReport(report) {
|
|
|
165
198
|
: ' COMMAND TOOLS';
|
|
166
199
|
console.log(scope);
|
|
167
200
|
const top = cmdRows.reduce((m, r) => Math.max(m, r.count), 1);
|
|
168
|
-
const crows = cmdRows.slice(0,
|
|
201
|
+
const crows = cmdRows.slice(0, ROW_CAP).map((r, idx) => [
|
|
169
202
|
String(idx + 1),
|
|
170
203
|
r.name,
|
|
171
204
|
fmt(r.count),
|
|
@@ -177,6 +210,8 @@ function printReport(report) {
|
|
|
177
210
|
for (const l of textTable(['#', 'Tool/Command', 'Count', 'Saved', 'Avg%', 'Time', 'Impact'], crows, [true, false, true, true, true, true, false], 30)) {
|
|
178
211
|
console.log(l);
|
|
179
212
|
}
|
|
213
|
+
if (cmdRows.length > ROW_CAP)
|
|
214
|
+
console.log(` showing top ${ROW_CAP} of ${fmt(cmdRows.length)} commands — full set in the dashboard`);
|
|
180
215
|
}
|
|
181
216
|
}
|
|
182
217
|
async function runUsage() {
|