@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/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, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
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 { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
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
- updateVersion: checkForUpdate(),
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 [[ompVersion, agentEntries, extEntries, sharedStateText, configText, updateVersion], [cavemanIndexText, rtkIndexText, updaterIndexText, comboIndexText, tersioIndexText, modeReinforcementText, ponytailPkgText, ponytailExtText, pluginsPkgRaw, selfPkgText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
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
- const usageRows = readUsage();
137
- check('Usage ledger', true, usageRows.length ? `${usageRows.length} rows` : 'empty — no records yet');
138
- console.log(` Usage ledger (tersio-owned, tersio reset clears): ${ledgerPath()} · ${usageRows.length} rows`);
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
- check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
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: 'omp plugin install + npm refresh' },
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 — refresh plugin');
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
- pkg.dependencies['@dietrichgebert/ponytail'] = PONYTAIL_GITHUB_SPEC;
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: extension already present and no refresh asked — skip network.
53
- let ponytailExtExists = !options.reinstall && !options.dryRun ? await probeExt() : false;
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(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
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
- // Try omp plugin install first
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 plugin', OMP_BIN, ['plugin', 'install', PONYTAIL_GITHUB_SPEC], { cwd: pluginsDir });
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
- if (options.reinstall) {
79
+ catch {
75
80
  try {
76
- await execNetwork('Refreshing Ponytail package', 'npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 });
81
+ await execNetwork('Installing bundled Ponytail', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
77
82
  }
78
83
  catch (e) {
79
- console.log(` [fail] Could not refresh ponytail: ${e.message}`);
80
- console.log(` [hint] Manual: cd ~/.omp/plugins && npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
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: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
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 'gain':
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/gain reports.
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/gain reports.
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/gain reports)', CURRENCY_CODES.map((v) => ({
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 gain dashboard owns displaying and
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
- 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,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
- 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
- console.log(' Currency applies to usage/gain reports; --currency overrides it per run.');
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 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() {
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) {