@krtclcdy/tersio 2.24.0 → 2.24.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.
Files changed (49) hide show
  1. package/README.md +1 -3
  2. package/cli/banner.js +2 -3
  3. package/cli/currency.js +3 -7
  4. package/cli/dashboard.js +167 -3
  5. package/cli/doctor-fix.js +15 -12
  6. package/cli/doctor.js +1 -12
  7. package/cli/hosts.js +7 -11
  8. package/cli/install.js +113 -71
  9. package/cli/interactive.js +36 -2
  10. package/cli/layer.js +1 -3
  11. package/cli/manifest.js +1 -4
  12. package/cli/profile.js +9 -7
  13. package/cli/reset.js +3 -7
  14. package/cli/rtk-wiring.js +1 -6
  15. package/cli/settings.js +3 -8
  16. package/cli/uninstall.js +43 -24
  17. package/cli/update.js +7 -8
  18. package/cli/usage.js +15 -5
  19. package/dashboard/dist/index.html +18 -18
  20. package/extensions/ai-addons-updater/index.js +6 -5
  21. package/extensions/ai-addons-updater/index.ts +7 -6
  22. package/extensions/caveman-session/index.js +14 -29
  23. package/extensions/caveman-session/index.ts +15 -30
  24. package/extensions/combo-toggle/index.js +10 -14
  25. package/extensions/combo-toggle/index.ts +10 -15
  26. package/extensions/lib/utils.js +4 -0
  27. package/extensions/lib/utils.ts +3 -0
  28. package/extensions/rtk-session/index.js +27 -31
  29. package/extensions/rtk-session/index.ts +25 -29
  30. package/extensions/shared/carbon.js +3 -8
  31. package/extensions/shared/carbon.ts +4 -8
  32. package/extensions/shared/host.js +1 -11
  33. package/extensions/shared/host.ts +1 -11
  34. package/extensions/shared/plugin-settings.js +4 -14
  35. package/extensions/shared/plugin-settings.ts +4 -14
  36. package/extensions/shared/pricing.js +4 -6
  37. package/extensions/shared/pricing.ts +4 -6
  38. package/extensions/shared/rtk-gain.js +2 -7
  39. package/extensions/shared/rtk-gain.ts +2 -7
  40. package/extensions/shared/session-state.js +30 -18
  41. package/extensions/shared/session-state.ts +33 -16
  42. package/extensions/shared/usage-ledger.js +138 -17
  43. package/extensions/shared/usage-ledger.ts +143 -19
  44. package/extensions/shared/usage-store.js +175 -23
  45. package/extensions/shared/usage-store.ts +171 -22
  46. package/extensions/tersio-commands/index.js +1 -5
  47. package/extensions/tersio-commands/index.ts +1 -5
  48. package/package.json +6 -7
  49. package/tersio.js +1 -3
package/cli/uninstall.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
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';
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';
@@ -22,7 +22,7 @@ const PI_TREE_DIRS = [
22
22
  // One `pi remove`, for both packages the installer adds there.
23
23
  async function piRemove(spec, shouldDryRun) {
24
24
  if (shouldDryRun) {
25
- console.log(` [dry-run] would run: pi remove ${spec}`);
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
- console.log(` [ok] removed the pi package ${spec}`);
38
+ sayTagged(` [ok] removed the pi package ${spec}`);
39
39
  }
40
40
  catch (e) {
41
- console.log(` [fail] pi remove: ${e.message}`);
42
- console.log(` [hint] Manual: pi remove ${spec}`);
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 owns its package dir, so removal is `pi remove`; the tree and the defaults
56
- // file are ours. Ponytail goes too unless --keep-ponytail, matching the OMP
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
- console.log(` [skip] ${host.label} — nothing installed (${host.installCmd})`);
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,12 @@ 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
83
+ // --remove-rtk on pi while OMP removed its copy.
84
+ if (shouldRemoveRtk) {
85
+ await removeUninstallTarget(rtkWiring, shouldDryRun, false);
86
+ await removeUninstallTarget(path.join(BUN_BIN_DIR, RTK_BINARY_NAME), shouldDryRun, false);
87
+ }
71
88
  await clearSessionDefaults(shouldDryRun);
72
89
  return true;
73
90
  }
@@ -91,11 +108,11 @@ async function updateJsonFile(filePath, mutate, dryRunNote, writeNote, dryRun) {
91
108
  if (!mutate(data))
92
109
  return;
93
110
  if (dryRun) {
94
- console.log(` [dry-run] ${dryRunNote}`);
111
+ sayTagged(` [dry-run] ${dryRunNote}`);
95
112
  return;
96
113
  }
97
114
  await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
98
- console.log(` [write] ${writeNote}`);
115
+ sayTagged(` [write] ${writeNote}`);
99
116
  }
100
117
  function dropKey(section, key) {
101
118
  if (!section || typeof section !== 'object')
@@ -109,14 +126,14 @@ function dropKey(section, key) {
109
126
  async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
110
127
  try {
111
128
  if (shouldDryRun) {
112
- console.log(` [dry-run] would remove ${target}`);
129
+ sayTagged(` [dry-run] would remove ${target}`);
113
130
  return;
114
131
  }
115
132
  if (recursive)
116
133
  await fs.rm(target, { recursive: true, force: true });
117
134
  else
118
135
  await fs.unlink(target);
119
- console.log(` [rm] ${target}`);
136
+ sayTagged(` [rm] ${target}`);
120
137
  }
121
138
  catch {
122
139
  debug(`Could not remove ${target}`);
@@ -163,14 +180,14 @@ async function runUninstall(options = {}) {
163
180
  }
164
181
  const selected = detectHosts().find((h) => h.id === host);
165
182
  if (host === 'pi') {
166
- const removed = await removePiLayer(selected, shouldDryRun, shouldRemovePonytail);
183
+ const removed = await removePiLayer(selected, shouldDryRun, shouldRemovePonytail, shouldRemoveRtk, confirmed);
167
184
  if (removed)
168
- console.log('\nDone. Restart pi for changes to take effect.');
185
+ console.log('\nDone. Restart pi, then /combo medium.');
169
186
  closeRL();
170
187
  return removed;
171
188
  }
172
189
  if (!selected.installed)
173
- console.log(` [note] ${selected.label} has no tersio install; removing what is left behind.`);
190
+ sayTagged(` [note] ${selected.label} has no tersio install; removing what is left behind.`);
174
191
  const extDir = path.join(OMP_AGENT_DIR, 'extensions');
175
192
  const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
176
193
  const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
@@ -222,23 +239,27 @@ async function runUninstall(options = {}) {
222
239
  });
223
240
  if (lines.length !== before) {
224
241
  if (shouldDryRun)
225
- console.log(` [dry-run] would remove ${before - lines.length} config.yml entries`);
242
+ sayTagged(` [dry-run] would remove ${before - lines.length} config.yml entries`);
226
243
  else
227
244
  await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
228
245
  }
229
246
  }
247
+ // Cleared last: writeConfigLines above would just make a fresh one.
248
+ for (const backup of [`${configPath}.bak`, path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'extensions', 'caveman-session', 'rule.md.bak')]) {
249
+ await removeUninstallTarget(backup, shouldDryRun, false);
250
+ }
230
251
  // Remove the bundled Ponytail copy; its config.yml entry is already filtered.
231
252
  if (shouldRemovePonytail) {
232
253
  const pluginsPkgPath = path.join(pluginsDir, 'package.json');
233
254
  await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
234
255
  try {
235
256
  if (shouldDryRun)
236
- console.log(` [dry-run] would remove ${ponytailPkgDir}`);
257
+ sayTagged(` [dry-run] would remove ${ponytailPkgDir}`);
237
258
  else {
238
259
  await fs.rm(ponytailPkgDir, { recursive: true, force: true });
239
- console.log(` [rm] ${ponytailPkgDir}`);
260
+ sayTagged(` [rm] ${ponytailPkgDir}`);
240
261
  await fs.rm(path.dirname(ponytailPkgDir));
241
- console.log(` [rm] ${path.dirname(ponytailPkgDir)} (empty scope)`);
262
+ sayTagged(` [rm] ${path.dirname(ponytailPkgDir)} (empty scope)`);
242
263
  }
243
264
  }
244
265
  catch {
@@ -254,9 +275,7 @@ async function runUninstall(options = {}) {
254
275
  if ((await readTextIfExists(path.join(selfPluginDir, 'package.json'))) !== null) {
255
276
  await removeUninstallTarget(selfPluginDir, shouldDryRun);
256
277
  }
257
- // Our rtk wiring goes with the rest: leaving the file would keep OMP
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.
278
+ // Our rtk wiring goes too, or OMP keeps rewriting for a removed package.
260
279
  await removeUninstallTarget(path.join(extDir, 'rtk.ts'), shouldDryRun);
261
280
  if (shouldRemoveRtk)
262
281
  await removeUninstallTarget(rtkBin, shouldDryRun, false);
package/cli/update.js CHANGED
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
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';
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';
@@ -140,8 +140,8 @@ async function runLatestUpdate() {
140
140
  const { stale, status } = planLines(plan);
141
141
  if (dryRun) {
142
142
  console.log(`tersio update (dry-run):\n ${status.join('\n ')}`);
143
- console.log(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
144
- console.log(` [dry-run] would delegate: npm exec --yes --prefer-online --package=${PACKAGE_NAME}${target} -- tersio --apply-update ${forwardedArgs.join(' ')}`);
143
+ sayTagged(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
144
+ sayTagged(` [dry-run] would delegate: npm exec --yes --prefer-online --package=${PACKAGE_NAME}${target} -- tersio --apply-update ${forwardedArgs.join(' ')}`);
145
145
  return;
146
146
  }
147
147
  console.log(`Checking for updates:\n ${status.join('\n ')}`);
@@ -186,8 +186,8 @@ async function runLatestUpdate() {
186
186
  process.stderr.write(globalResult.stderr);
187
187
  }
188
188
  catch (e) {
189
- console.log(` [warn] Global CLI update skipped: ${e.message}`);
190
- console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
189
+ sayTagged(` [warn] Global CLI update skipped: ${e.message}`);
190
+ sayTagged(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
191
191
  }
192
192
  const npmArgs = [
193
193
  'exec',
@@ -200,9 +200,8 @@ async function runLatestUpdate() {
200
200
  ...forwardedArgs,
201
201
  ];
202
202
  const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
203
- // Inherited stdio, no outer spinner. The delegated installer runs quiet
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.
203
+ // Inherited stdio, no outer spinner; the delegated installer runs quiet and
204
+ // this parent owns both the plan line and the closing summary.
206
205
  try {
207
206
  await execInherit(npmCommand, npmCommandArgs);
208
207
  console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart OMP.`);
package/cli/usage.js CHANGED
@@ -113,6 +113,7 @@ function bar(frac, width = 12) {
113
113
  const filled = Math.max(0, Math.min(width, Math.round(frac * width)));
114
114
  return '█'.repeat(filled) + '░'.repeat(width - filled);
115
115
  }
116
+ const ROW_CAP = 15;
116
117
  function pad(s, n) {
117
118
  return s.length >= n ? s : s + ' '.repeat(n - s.length);
118
119
  }
@@ -136,10 +137,15 @@ function printReport(report) {
136
137
  return;
137
138
  }
138
139
  const t = report.tokens;
139
- console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`);
140
- 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.)`);
141
- console.log(` RTK ${fmt(report.rtkAdoption.rtkCalls)}/${fmt(report.rtkAdoption.eligibleCalls)} eligible OMP Bash calls use RTK (${report.rtkAdoption.adoptionPct.toFixed(1)}%) · ${fmt(report.rtkAdoption.missedCalls)} missed · recall ${report.rtkRecall.available ? `${report.rtkRecall.mode} (${report.rtkRecall.entries})` : 'unavailable'}`);
142
- 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);
140
+ const a = report.rtkAdoption;
141
+ for (const l of textTable(['Metric', 'Value'], [
142
+ ['Tokens', `${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`],
143
+ ['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.)`],
144
+ ['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'}`],
145
+ ], [false, false], 118))
146
+ console.log(l);
147
+ 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));
148
+ const models = allModels.slice(0, ROW_CAP);
143
149
  if (models.length) {
144
150
  console.log(' BY MODEL');
145
151
  const top = models.reduce((m, [, b]) => Math.max(m, b.input + b.output + b.cacheRead + b.cacheWrite), 1);
@@ -152,6 +158,8 @@ function printReport(report) {
152
158
  for (const l of textTable(['Model', 'Input', 'Output', 'Cache r/w', report.currency, 'Hit', 'Share'], mrows, [false, true, true, true, true, true, false])) {
153
159
  console.log(l);
154
160
  }
161
+ if (allModels.length > ROW_CAP)
162
+ console.log(` showing top ${ROW_CAP} of ${fmt(allModels.length)} models — full set in the dashboard`);
155
163
  }
156
164
  const cmdRows = report.byTool.map(([tool, n]) => ({ name: tool, count: n, saved: null, avgPct: null, avgMs: null }));
157
165
  for (const r of report.rtkGain.byCommand) {
@@ -165,7 +173,7 @@ function printReport(report) {
165
173
  : ' COMMAND TOOLS';
166
174
  console.log(scope);
167
175
  const top = cmdRows.reduce((m, r) => Math.max(m, r.count), 1);
168
- const crows = cmdRows.slice(0, 15).map((r, idx) => [
176
+ const crows = cmdRows.slice(0, ROW_CAP).map((r, idx) => [
169
177
  String(idx + 1),
170
178
  r.name,
171
179
  fmt(r.count),
@@ -177,6 +185,8 @@ function printReport(report) {
177
185
  for (const l of textTable(['#', 'Tool/Command', 'Count', 'Saved', 'Avg%', 'Time', 'Impact'], crows, [true, false, true, true, true, true, false], 30)) {
178
186
  console.log(l);
179
187
  }
188
+ if (cmdRows.length > ROW_CAP)
189
+ console.log(` showing top ${ROW_CAP} of ${fmt(cmdRows.length)} commands — full set in the dashboard`);
180
190
  }
181
191
  }
182
192
  async function runUsage() {