@krtclcdy/tersio 2.20.1 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/settings.js CHANGED
@@ -4,11 +4,12 @@
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)
@@ -71,6 +72,94 @@ 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/gain 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
165
  // Currency lives here no longer: the gain dashboard owns displaying and
@@ -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
- if (profileFlagsGiven || currencyGiven) {
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,20 +216,28 @@ 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
- console.log(' Usage: tersio settings [--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] [--dry-run]');
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
- console.log(` Defaults: ${formatProfile(next)}`);
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
242
  console.log(' Currency applies to usage/gain reports; --currency overrides it per run.');
120
243
  closeRL();
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 ships with Tersio's presets, so a full uninstall removes it too;
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 plugin package the installer added (dep, files,
139
- // lock entry); the config.yml entry was filtered above.
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 = path.join(OMP_PLUGINS_DIR, 'tersio-update-check.json');
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, ponytailLatest] = await Promise.all([
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: [ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null, ponytailLatest],
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
- push('Ponytail', plan.ponytail[0], plan.ponytail[1]);
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() {
@@ -166,6 +156,25 @@ async function runLatestUpdate() {
166
156
  return;
167
157
  }
168
158
  console.log(`Updating ${stale.join(', ')}`);
159
+ // Broken payloads (v2.20.0) crash on boot — smoke-check the target before touching anything installed.
160
+ const smokeArgs = IS_WINDOWS
161
+ ? ['/d', '/s', '/c', 'npm', 'exec', '--yes', '--prefer-online', `--package=${PACKAGE_NAME}${target}`, '--', PACKAGE_BIN, '--version']
162
+ : ['exec', '--yes', '--prefer-online', `--package=${PACKAGE_NAME}${target}`, '--', PACKAGE_BIN, '--version'];
163
+ try {
164
+ await execP(npmCommand, smokeArgs, {
165
+ timeout: 120000,
166
+ maxBuffer: 1024 * 1024,
167
+ windowsHide: true,
168
+ shell: false,
169
+ });
170
+ }
171
+ catch (e) {
172
+ console.error(`[fail] Update payload ${PACKAGE_NAME}${target} failed its smoke check — staying on ${PACKAGE_VERSION}.`);
173
+ console.error(`[fail] ${e.message.split('\n')[0]}`);
174
+ console.error(`[hint] Retry once released fixed; manual: npm install -g ${PACKAGE_NAME}@latest --no-audit --no-fund --prefer-online`);
175
+ process.exitCode = 1;
176
+ return;
177
+ }
169
178
  // The npx delegation below only refreshes the OMP-side files; the globally
170
179
  // installed CLI keeps its old version until npm -g runs. Refresh both.
171
180
  const globalArgs = IS_WINDOWS
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${t.cacheWrite > 0 ? ` · ${fmt(t.cacheWrite)} written` : ''}`);
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) {