@krtclcdy/tersio 2.22.0 → 2.23.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.
Files changed (47) hide show
  1. package/README.md +38 -30
  2. package/cli/common.js +43 -39
  3. package/cli/currency.js +2 -2
  4. package/cli/dashboard.js +60 -116
  5. package/cli/doctor-fix.js +35 -29
  6. package/cli/doctor.js +26 -15
  7. package/cli/install.js +15 -34
  8. package/cli/profile.js +2 -1
  9. package/cli/settings.js +6 -6
  10. package/cli/update.js +12 -11
  11. package/cli/usage.js +6 -1
  12. package/dashboard/dist/index.html +65 -0
  13. package/extensions/ai-addons-updater/index.js +7 -3
  14. package/extensions/ai-addons-updater/index.ts +15 -3
  15. package/extensions/caveman-session/index.js +17 -12
  16. package/extensions/caveman-session/index.ts +17 -12
  17. package/extensions/caveman-session/rule.md +90 -0
  18. package/extensions/combo-toggle/index.js +22 -2
  19. package/extensions/combo-toggle/index.ts +26 -2
  20. package/extensions/lib/utils.js +20 -4
  21. package/extensions/lib/utils.ts +18 -4
  22. package/extensions/rtk-session/index.js +11 -10
  23. package/extensions/rtk-session/index.ts +10 -11
  24. package/extensions/shared/plugin-settings.js +37 -3
  25. package/extensions/shared/plugin-settings.ts +34 -3
  26. package/extensions/shared/rtk-gain.js +2 -2
  27. package/extensions/shared/rtk-gain.ts +3 -3
  28. package/extensions/shared/session-state.js +5 -3
  29. package/extensions/shared/session-state.ts +4 -3
  30. package/extensions/shared/types.ts +5 -0
  31. package/extensions/shared/usage-ledger.js +114 -4
  32. package/extensions/shared/usage-ledger.ts +122 -4
  33. package/extensions/tersio-commands/index.js +10 -13
  34. package/extensions/tersio-commands/index.ts +10 -13
  35. package/package.json +21 -12
  36. package/tersio.js +10 -10
  37. package/dashboard/charts.js +0 -1208
  38. package/dashboard/core.js +0 -357
  39. package/dashboard/settings.js +0 -166
  40. package/dashboard/share.js +0 -218
  41. package/dashboard/styles.css +0 -379
  42. package/dashboard/template.html +0 -515
  43. package/extensions/shared/mode-reinforcement.ts +0 -29
  44. package/gain/dist/brand.webp +0 -0
  45. package/gain/dist/index.html +0 -65
  46. package/gain/dist/vite.svg +0 -1
  47. /package/dashboard/{brand.webp → dist/brand.webp} +0 -0
package/cli/doctor-fix.js CHANGED
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, dryRun, verbose, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, execP, readPluginsPackage, writeIfChanged, } from './common.js';
5
+ import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, execP, readPluginsPackage, writeIfChanged, } from './common.js';
6
6
  import { execNetwork } from './interactive.js';
7
7
  import { wireRtkOmp, ensureRtkInConfig } from './rtk-wiring.js';
8
8
  import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
@@ -17,33 +17,40 @@ const SOURCES = [
17
17
  [path.join(EXT_DIR, 'shared', 'pricing.ts'), path.join('shared', 'pricing.ts')],
18
18
  [path.join(EXT_DIR, 'shared', 'carbon.ts'), path.join('shared', 'carbon.ts')],
19
19
  [path.join(EXT_DIR, 'caveman-session', 'index.ts'), path.join('caveman-session', 'index.ts')],
20
+ [path.join(EXT_DIR, 'caveman-session', 'rule.md'), path.join('caveman-session', 'rule.md')],
20
21
  [path.join(EXT_DIR, 'rtk-session', 'index.ts'), path.join('rtk-session', 'index.ts')],
21
22
  [path.join(EXT_DIR, 'ai-addons-updater', 'index.ts'), path.join('ai-addons-updater', 'index.ts')],
22
23
  [path.join(EXT_DIR, 'combo-toggle', 'index.ts'), path.join('combo-toggle', 'index.ts')],
23
24
  [path.join(EXT_DIR, 'tersio-commands', 'index.ts'), path.join('tersio-commands', 'index.ts')],
24
- [path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts'), path.join('shared', 'mode-reinforcement.ts')],
25
25
  ];
26
- async function fixExtensions(extDir) {
27
- console.log(' Doctor --fix: restoring extension files');
28
- await fs.mkdir(extDir, { recursive: true });
29
- const texts = await Promise.all(SOURCES.map(([from]) => readTextIfExists(from)));
30
- const writes = [];
31
- for (let i = 0; i < SOURCES.length; i++) {
32
- if (texts[i] !== null)
33
- writes.push(writeIfChanged(path.join(extDir, SOURCES[i][1]), texts[i], { dryRun, verbose }));
26
+ async function fixExtensions(pluginsDir) {
27
+ console.log(' Doctor --fix: restoring plugin extension files');
28
+ const tersioPluginDir = path.join(pluginsDir, 'node_modules', '@krtclcdy', 'tersio');
29
+ const pluginExtDir = path.join(tersioPluginDir, 'extensions');
30
+ await fs.mkdir(pluginExtDir, { recursive: true });
31
+ for (const [from, to] of SOURCES) {
32
+ const text = await readTextIfExists(from);
33
+ if (text === null)
34
+ console.log(` [warn] bundled source missing: ${from}`);
34
35
  else
35
- console.log(` [warn] bundled source missing: ${SOURCES[i][0]}`);
36
+ await writeIfChanged(path.join(pluginExtDir, to), text, { dryRun, verbose });
36
37
  }
37
- await Promise.all(writes);
38
- if ((await readTextIfExists(path.join(extDir, 'caveman-session', 'rule.md'))) === null) {
39
- const rule = await Promise.race([
40
- httpsGet(CAVEMAN_REMOTE_RULE).catch(() => null),
41
- new Promise((resolve) => setTimeout(() => resolve(null), 8000)),
42
- ]);
43
- if (rule !== null)
44
- await writeIfChanged(path.join(extDir, 'caveman-session', 'rule.md'), rule, { dryRun, verbose });
45
- else
46
- console.log(' [warn] Caveman rule unreachable — kept existing rule.md');
38
+ const ruleDest = path.join(pluginExtDir, 'caveman-session', 'rule.md');
39
+ if ((await readTextIfExists(ruleDest)) === null) {
40
+ const bundled = await readTextIfExists(path.join(EXT_DIR, 'caveman-session', 'rule.md'));
41
+ if (bundled !== null) {
42
+ await writeIfChanged(ruleDest, bundled, { dryRun, verbose });
43
+ }
44
+ else {
45
+ const rule = await Promise.race([
46
+ httpsGet(CAVEMAN_REMOTE_RULE).catch(() => null),
47
+ new Promise((resolve) => setTimeout(() => resolve(null), 8000)),
48
+ ]);
49
+ if (rule !== null)
50
+ await writeIfChanged(ruleDest, rule, { dryRun, verbose });
51
+ else
52
+ console.log(' [warn] Caveman rule unreachable and no bundled rule exists');
53
+ }
47
54
  }
48
55
  }
49
56
  async function fixRegistrations(agentDir, pluginsDir) {
@@ -58,13 +65,13 @@ async function fixRegistrations(agentDir, pluginsDir) {
58
65
  }
59
66
  }
60
67
  const extDir = path.join(agentDir, 'extensions');
61
- const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
62
- await ensureExtensionInConfig(configPath, ponytailExtPath, 'ponytail', { dryRun, verbose });
63
- await ensureExtensionInConfig(configPath, path.join(extDir, 'combo-toggle', 'index.ts'), 'combo', { dryRun, verbose });
64
68
  const rtkTs = path.join(extDir, 'rtk.ts');
65
69
  if ((await readTextIfExists(rtkTs)) !== null)
66
70
  await ensureRtkInConfig({ dryRun, verbose });
67
- await ensureExtensionAfterConfigEntry(configPath, path.join(extDir, 'shared', 'mode-reinforcement.ts'), ponytailExtPath, 'mode reinforcement', { dryRun, verbose });
71
+ const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
72
+ await removeExtensionFromConfig(configPath, path.join(extDir, 'combo-toggle', 'index.ts'), 'manifest-loaded combo', { dryRun, verbose });
73
+ await removeExtensionFromConfig(configPath, ponytailExtPath, 'manifest-loaded ponytail', { dryRun, verbose });
74
+ await removeExtensionFromConfig(configPath, path.join(extDir, 'shared', 'mode-reinforcement.ts'), 'retired mode reinforcement', { dryRun, verbose });
68
75
  const pkgPath = path.join(pluginsDir, 'package.json');
69
76
  const pkg = await readPluginsPackage(pkgPath);
70
77
  if (!(PACKAGE_NAME in pkg.dependencies)) {
@@ -127,7 +134,7 @@ async function fixRtk(binDir) {
127
134
  if (!(await wireRtkOmp(binDest, { dryRun, verbose })))
128
135
  throw new Error('rtk wiring failed');
129
136
  }
130
- async function fixPonytail(pluginsDir, agentDir) {
137
+ async function fixPonytail(pluginsDir) {
131
138
  console.log(' Doctor --fix: restoring bundled Ponytail');
132
139
  await fs.mkdir(pluginsDir, { recursive: true });
133
140
  const pkgPath = path.join(pluginsDir, 'package.json');
@@ -147,7 +154,6 @@ async function fixPonytail(pluginsDir, agentDir) {
147
154
  await execNetwork('Restoring bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
148
155
  throw new Error(`bundled ponytail restore failed: ${e.message}`);
149
156
  });
150
- await ensureExtensionInConfig(path.join(agentDir, 'config.yml'), path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'), 'ponytail', { dryRun, verbose });
151
157
  }
152
158
  async function fixCli() {
153
159
  console.log(' Doctor --fix: updating CLI + add-ons');
@@ -171,13 +177,13 @@ async function runDoctorRepairs(targets) {
171
177
  }
172
178
  };
173
179
  if (want('extensions'))
174
- await attempt('extensions', () => fixExtensions(path.join(OMP_AGENT_DIR, 'extensions')));
180
+ await attempt('extensions', () => fixExtensions(OMP_PLUGINS_DIR));
175
181
  if (want('registrations'))
176
182
  await attempt('registrations', () => fixRegistrations(OMP_AGENT_DIR, OMP_PLUGINS_DIR));
177
183
  if (want('rtk'))
178
184
  await attempt('rtk', () => fixRtk(BUN_BIN_DIR));
179
185
  if (want('ponytail'))
180
- await attempt('ponytail', () => fixPonytail(OMP_PLUGINS_DIR, OMP_AGENT_DIR));
186
+ await attempt('ponytail', () => fixPonytail(OMP_PLUGINS_DIR));
181
187
  if (want('cli'))
182
188
  await attempt('cli', fixCli);
183
189
  return failed;
package/cli/doctor.js CHANGED
@@ -1,11 +1,11 @@
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, RTK_BINARY_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
4
+ import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
5
5
  import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
6
6
  import { usageDbPath } from '../extensions/shared/usage-store.js';
7
7
  import { pricesCachePath } from '../extensions/shared/pricing.js';
8
- import { readTextIfExists } from '../extensions/lib/utils.js';
8
+ import { readTextIfExists, resolveRtkBinary } from '../extensions/lib/utils.js';
9
9
  async function runDoctor(recheck = false) {
10
10
  console.log('\n=== Tersio Doctor ===');
11
11
  // Directories
@@ -13,38 +13,39 @@ async function runDoctor(recheck = false) {
13
13
  const extDir = path.join(agentDir, 'extensions');
14
14
  const configPath = path.join(agentDir, 'config.yml');
15
15
  const pluginsDir = OMP_PLUGINS_DIR;
16
- const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
16
+ const rtkBin = resolveRtkBinary();
17
17
  // Ponytail
18
18
  const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
19
19
  const ponytailExt = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
20
- const cavemanIndex = path.join(extDir, 'caveman-session', 'index.ts');
21
- const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
22
- const rtkIndex = path.join(extDir, 'rtk-session', 'index.ts');
23
- const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.ts');
20
+ const tersioPluginDir = path.join(pluginsDir, 'node_modules', '@krtclcdy', 'tersio');
21
+ const cavemanIndex = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts');
22
+ const cavemanRule = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'rule.md');
23
+ const rtkIndex = path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts');
24
+ const updaterIndex = path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts');
24
25
  // Independent probes start concurrently; sections report in fixed order as
25
26
  // their data settles. Every probe resolves instead of rejecting.
26
27
  const probes = {
27
28
  agentEntries: fs.readdir(agentDir).catch(() => null),
28
29
  extEntries: fs.readdir(extDir).catch(() => null),
29
- sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.ts')),
30
+ sharedStateText: readTextIfExists(path.join(tersioPluginDir, 'extensions', 'shared', 'session-state.ts')),
30
31
  configText: readTextIfExists(configPath),
31
32
  ponytailPkgText: readTextIfExists(ponytailPkg),
32
33
  ponytailExtText: readTextIfExists(ponytailExt),
33
- rtkBinText: readTextIfExists(rtkBin),
34
+ rtkBinText: rtkBin ? readTextIfExists(rtkBin) : Promise.resolve(null),
34
35
  rtkOmpText: readTextIfExists(path.join(extDir, 'rtk.ts')),
35
36
  cavemanIndexText: readTextIfExists(cavemanIndex),
36
37
  cavemanRuleText: readTextIfExists(cavemanRule),
37
38
  rtkIndexText: readTextIfExists(rtkIndex),
38
39
  updaterIndexText: readTextIfExists(updaterIndex),
40
+ rtkMtime: rtkBin ? fs.stat(rtkBin).catch(() => null) : Promise.resolve(null),
39
41
  ruleMtime: fs.stat(cavemanRule).catch(() => null),
40
- rtkMtime: fs.stat(rtkBin).catch(() => null),
41
42
  ponytailMtime: fs.stat(ponytailPkg).catch(() => null),
42
43
  pricesMtime: fs.stat(pricesCachePath()).catch(() => null),
43
44
  };
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) => {
45
+ const rtkVersionProbe = rtkBin ? execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => r.stdout.trim() || r.stderr.trim() || null, (e) => {
45
46
  const err = e;
46
47
  return err.stdout?.trim() || err.stderr?.trim() || null;
47
- }));
48
+ }) : Promise.resolve(null);
48
49
  const [[agentEntries, extEntries, sharedStateText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText, ponytailExtText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
49
50
  Promise.all([
50
51
  probes.agentEntries,
@@ -101,6 +102,16 @@ async function runDoctor(recheck = false) {
101
102
  check('OMP config.yml', configText !== null, configText === null ? configPath : '');
102
103
  check('Shared session bridge', sharedStateText !== null);
103
104
  section('Extensions & plugins');
105
+ const explicitEntries = (configText ?? '').split('\n')
106
+ .map((line) => line.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, ''))
107
+ .filter((line) => line.startsWith('/') || line.startsWith('.'));
108
+ const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
109
+ const retiredReinforcement = explicitEntries.filter((entry) => entry.endsWith('/shared/mode-reinforcement.ts')).length;
110
+ check('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.join(', '));
111
+ if (retiredReinforcement === 0)
112
+ check('No retired reinforcement', true);
113
+ else
114
+ warnLine('Retired reinforcement registration', `${retiredReinforcement} found; run doctor --fix registrations`);
104
115
  check('Caveman extension', cavemanIndexText !== null);
105
116
  check('RTK extension', rtkIndexText !== null);
106
117
  check('Updater extension', updaterIndexText !== null);
@@ -113,7 +124,7 @@ async function runDoctor(recheck = false) {
113
124
  const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
114
125
  check('Caveman rule', cavemanRuleText !== null, ruleAge);
115
126
  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(' '));
127
+ check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
117
128
  if (rtkBinText !== null && !rtkVersion)
118
129
  warnLine('RTK version', 'unavailable — binary may not be executable');
119
130
  const rtkRegistered = rtkOmpText !== null && (configText ?? '').includes('extensions/rtk.ts');
@@ -164,7 +175,7 @@ async function runDoctorFix(tally) {
164
175
  if (tally.warn > 0)
165
176
  problems.push(`${tally.warn} warn`);
166
177
  if (dryRun) {
167
- console.log(`\n [dry-run] would repair ${problems.join(' + ')} (extensions, config.yml registrations, rtk wiring, ponytail refresh, CLI update)`);
178
+ console.log(`\n [dry-run] would repair ${problems.join(' + ')} (extensions, stale config.yml entries, rtk wiring, ponytail refresh, CLI update)`);
168
179
  return;
169
180
  }
170
181
  const scoped = fixScope();
@@ -186,7 +197,7 @@ async function runDoctorFix(tally) {
186
197
  const picked = await askInteractiveChoice('Doctor — what should I repair?', [
187
198
  { value: 'all', label: 'Fix everything', hint: `${tally.missing + tally.warn} rows` },
188
199
  { value: 'extensions', label: 'Missing extension files', hint: 'copy sources from this CLI' },
189
- { value: 'registrations', label: 'config.yml registrations', hint: 'combo, ponytail, rtk.ts entries' },
200
+ { value: 'registrations', label: 'config.yml registrations', hint: 'remove stale entries; keep manifest-owned extensions out' },
190
201
  { value: 'rtk', label: 'RTK binary + wiring', hint: 'download checksum-verified binary, rtk init' },
191
202
  { value: 'ponytail', label: 'Ponytail package', hint: 'bundled reinstall via tersio dep' },
192
203
  { value: 'cli', label: 'CLI update', hint: 'latest tersio + delegated refresh' },
package/cli/install.js CHANGED
@@ -3,14 +3,13 @@ import { promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { BUN_BIN_DIR, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
6
+ import { BUN_BIN_DIR, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
7
7
  import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
8
8
  import { printWelcome } from './banner.js';
9
9
  import { checkForUpdate, runLatestUpdate } from './update.js';
10
10
  import { runUninstall } from './uninstall.js';
11
11
  import { runDoctor } from './doctor.js';
12
12
  import { runReset } from './reset.js';
13
- import { runSettings } from './settings.js';
14
13
  import { runUsage } from './usage.js';
15
14
  import { runDashboard } from './dashboard.js';
16
15
  import { wireRtkOmp } from './rtk-wiring.js';
@@ -20,18 +19,18 @@ import { storedProfile, writePluginSettings } from './profile.js';
20
19
  const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
21
20
  const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.ts');
22
21
  const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.ts');
22
+ const CAVEMAN_RULE = path.join(EXT_DIR, 'caveman-session', 'rule.md');
23
23
  const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.ts');
24
24
  const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.ts');
25
25
  const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.ts');
26
26
  const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.ts');
27
- const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts');
28
27
  const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.ts');
29
28
  const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.ts');
30
29
  const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts');
31
30
  const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
32
31
  const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
33
32
  const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
34
- async function stepPonytail(pluginsDir, userDir, options) {
33
+ async function stepPonytail(pluginsDir, options) {
35
34
  if (!options.quiet)
36
35
  console.log(' Ponytail — ensure bundled plugin');
37
36
  await fs.mkdir(pluginsDir, { recursive: true });
@@ -92,12 +91,8 @@ async function stepPonytail(pluginsDir, userDir, options) {
92
91
  console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
93
92
  }
94
93
  else if (!options.dryRun && !options.quiet) {
95
- // Wire extension into config.yml so /ponytail command loads
96
- debug('Bundled Ponytail pi-extension found');
94
+ debug('Bundled Ponytail pi-extension found; OMP loads it from the plugin manifest');
97
95
  }
98
- // Still set Ponytail config defaults even without the extension command
99
- const configPath = path.join(userDir, 'config.yml');
100
- await ensureExtensionInConfig(configPath, ponytailExtPath, 'ponytail', options);
101
96
  await ensurePonytailConfigValue('defaultMode', 'off', options);
102
97
  // hideStatus=true keeps the upstream ponytail bar hidden; combo owns the bar.
103
98
  await ensurePonytailConfigValue('hideStatus', true, options);
@@ -355,14 +350,6 @@ async function stepSharedSessionState(extDir, options) {
355
350
  [SHARED_CARBON, path.join('shared', 'carbon.ts')],
356
351
  ], 'shared/session-state.js', options);
357
352
  }
358
- async function stepModeReinforcement(extDir, ponytailExtPath, options) {
359
- if (!options.quiet)
360
- console.log(' Session helpers — sync shared files');
361
- const dest = path.join(extDir, 'shared', 'mode-reinforcement.ts');
362
- if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.ts')]], 'shared/mode-reinforcement.ts', options))
363
- return;
364
- await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
365
- }
366
353
  async function stepRtkSession(extDir, options) {
367
354
  if (!options.quiet)
368
355
  console.log(' RTK session — install session mode');
@@ -371,12 +358,17 @@ async function stepRtkSession(extDir, options) {
371
358
  // One rule fetch serves the install; dry runs stay offline
372
359
  // and fall back to the bundled rule to preview its destination.
373
360
  async function fetchCavemanRule(options) {
361
+ const bundled = await readTextIfExists(CAVEMAN_RULE);
374
362
  if (options.dryRun)
375
- return (await readTextIfExists(path.join(path.dirname(CAVEMAN_INDEX), 'rule.md'))) || '';
363
+ return bundled;
376
364
  try {
377
365
  return await withInteractiveSpinner('Fetching Caveman rule', () => httpsGet(CAVEMAN_REMOTE_RULE));
378
366
  }
379
367
  catch (e) {
368
+ if (bundled !== null) {
369
+ debug(`Using bundled Caveman rule: ${e.message}`);
370
+ return bundled;
371
+ }
380
372
  console.log(` [warn] Could not fetch caveman rule: ${e.message}`);
381
373
  return null;
382
374
  }
@@ -411,12 +403,7 @@ async function stepUpdater(extDir, options) {
411
403
  async function stepCombo(extDir, options) {
412
404
  if (!options.quiet)
413
405
  console.log(' Combo — install preset switch');
414
- const dest = path.join(extDir, 'combo-toggle', 'index.ts');
415
- if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.ts')]], 'combo-toggle/index.ts', options))
416
- return;
417
- // Auto-register combo in config.yml
418
- const configPath = path.join(path.dirname(extDir), 'config.yml');
419
- await ensureExtensionInConfig(configPath, dest, 'combo', options);
406
+ await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.ts')]], 'combo-toggle/index.ts', options);
420
407
  }
421
408
  async function resolveProfile(forceReinstall = false, opts = {}) {
422
409
  // Seed from the lock file so flag-less update/reinstall runs keep the
@@ -484,9 +471,8 @@ async function runCommandMenu() {
484
471
  { value: 'reinstall', label: 'Reinstall', hint: 'clean and reinstall the add-ons, Ponytail package kept' },
485
472
  { value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
486
473
  { value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
487
- { value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
474
+ { value: 'dashboard', label: 'Dashboard', hint: 'open the report in your browser' },
488
475
  { value: 'reset', label: 'Reset statistics', hint: 'clear statistics; transcripts and RTK history stay' },
489
- { value: 'settings', label: 'Settings', hint: 'defaults: combo, caveman, rtk, ponytail, currency' },
490
476
  { value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
491
477
  ], 'install');
492
478
  if (choice.status !== 'selected') {
@@ -545,7 +531,7 @@ async function runCommandMenu() {
545
531
  await runUsage();
546
532
  closeRL();
547
533
  break;
548
- case 'gain':
534
+ case 'dashboard':
549
535
  await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
550
536
  closeRL();
551
537
  break;
@@ -553,9 +539,6 @@ async function runCommandMenu() {
553
539
  await runReset();
554
540
  closeRL();
555
541
  break;
556
- case 'settings':
557
- await runSettings();
558
- break;
559
542
  case 'uninstall':
560
543
  await runUninstall();
561
544
  closeRL();
@@ -644,14 +627,12 @@ async function runInstall(overrides = {}) {
644
627
  await capture('shared', () => stepSharedSessionState(userExtDir, options));
645
628
  let selfPlugin = false;
646
629
  await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options); });
647
- await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
648
- const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
630
+ await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, options));
649
631
  await capture('rtk', () => stepRtk(BUN_BIN_DIR, options));
650
632
  await capture('rtk session', () => stepRtkSession(userExtDir, options));
651
- await capture('caveman', () => stepCaveman(userExtDir, cavemanRule, options));
633
+ await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'), cavemanRule, options));
652
634
  await capture('combo', () => stepCombo(userExtDir, options));
653
635
  await capture('commands', () => stepTersioCommands(userExtDir, options));
654
- await capture('reinforcement', () => stepModeReinforcement(userExtDir, ponytailExtPath, options));
655
636
  await capture('updater', () => stepUpdater(userExtDir, options));
656
637
  if (selfPlugin)
657
638
  await capture('settings', () => writePluginSettings(profile, 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';
@@ -72,6 +72,7 @@ async function writePluginSettings(profile, options) {
72
72
  config.settings[PACKAGE_NAME] = {
73
73
  ...(config.settings[PACKAGE_NAME] || {}),
74
74
  comboDefault: profile.comboDefault,
75
+ comboSetupComplete: true,
75
76
  cavemanDefault: profile.cavemanDefault,
76
77
  rtkDefault: profile.rtkDefault,
77
78
  ponytailDefault: profile.ponytailDefault,
package/cli/settings.js CHANGED
@@ -1,5 +1,5 @@
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.
@@ -64,7 +64,7 @@ async function askProfile(current) {
64
64
  if (ponytail.status !== 'selected')
65
65
  return null;
66
66
  next.ponytailDefault = ponytail.value;
67
- 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) => ({
68
68
  value: v, label: v,
69
69
  })), next.currency);
70
70
  if (cur.status !== 'selected')
@@ -85,7 +85,7 @@ async function askDiagSchedule(current) {
85
85
  }
86
86
  const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis'];
87
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]');
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-lite|wenyan-full|wenyan-ultra] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--dry-run]');
89
89
  }
90
90
  // Single-setting jump: `tersio settings diagnosis` prompts only that value
91
91
  // instead of walking the whole chain. Returns false when aborted.
@@ -138,7 +138,7 @@ async function runSingleSetting(name, current, nextDiag) {
138
138
  break;
139
139
  }
140
140
  case 'currency': {
141
- const picked = await askInteractiveChoice('Display currency (usage/gain reports)', CURRENCY_CODES.map((v) => ({
141
+ const picked = await askInteractiveChoice('Display currency (usage/dashboard reports)', CURRENCY_CODES.map((v) => ({
142
142
  value: v, label: v,
143
143
  })), next.currency);
144
144
  if (picked.status !== 'selected')
@@ -162,7 +162,7 @@ async function runSingleSetting(name, current, nextDiag) {
162
162
  }
163
163
  function printSettingsTable(current) {
164
164
  console.log('\n=== Tersio Settings ===');
165
- // Currency lives here no longer: the gain dashboard owns displaying and
165
+ // Currency lives here no longer: the dashboard owns displaying and
166
166
  // persisting it (its picker POSTs to /currency); settings can still set
167
167
  // the stored default via --currency or the prompt below.
168
168
  const rows = [
@@ -239,7 +239,7 @@ async function runSettings() {
239
239
  setDiagSchedule(nextDiag);
240
240
  console.log(` Defaults: ${formatProfile(next)} · diagnosis=${nextDiag}`);
241
241
  console.log(' Applies to fresh sessions only — anything persisted with /combo, /caveman, or /rtk wins.');
242
- 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.');
243
243
  closeRL();
244
244
  }
245
245
  export { runSettings };
package/cli/update.js CHANGED
@@ -2,11 +2,11 @@
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 { 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';
5
+ import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, dryRun, execP, parseJsonObject, verbose, } from './common.js';
6
6
  import { execNetwork } from './interactive.js';
7
- import { readTextIfExists, tersioDataPath } from '../extensions/lib/utils.js';
7
+ import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/lib/utils.js';
8
8
  import { refreshPrices } from '../extensions/shared/pricing.js';
9
- import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
9
+ import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
10
10
  const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
11
11
  function newerThan(a, b) {
12
12
  const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
@@ -83,21 +83,22 @@ async function execInherit(cmd, args) {
83
83
  // Current → latest per add-on. Every probe is capped and nullable; the plan
84
84
  // prints `unknown` for anything unreachable and the update proceeds anyway.
85
85
  async function probeUpdatePlan(cliLatest) {
86
- const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
86
+ const rtkBin = resolveRtkBinary();
87
87
  const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
88
- const ruleDest = path.join(OMP_AGENT_DIR, 'extensions', 'caveman-session', 'rule.md');
88
+ const tersioRule = path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman-session', 'rule.md');
89
+ const controller = new AbortController();
89
90
  const [rtkLocal, rtkRelease, ruleLocalText, ruleRemoteText, ponytailLocalText] = await Promise.all([
90
- settle(execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null), 6000),
91
- settle(fetchJson(RTK_RELEASE_API).then((r) => normalizeRtkVersion(r.tag_name) || null), 4000),
92
- readTextIfExists(ruleDest),
93
- settle(httpsGet(CAVEMAN_REMOTE_RULE), 4000),
91
+ settle(rtkBin ? execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null) : Promise.resolve(null), 6000),
92
+ settle(httpsGet(RTK_RELEASE_API, { signal: controller.signal }).then((text) => normalizeRtkVersion(JSON.parse(text).tag_name) || null), 1500),
93
+ readTextIfExists(tersioRule),
94
+ settle(httpsGet(CAVEMAN_REMOTE_RULE, { signal: controller.signal }).then((text) => sha256Hex(text).slice(0, 8)), 1500),
94
95
  readTextIfExists(ponytailPkg),
95
96
  ]);
96
- const shortHash = (text) => text === null ? null : sha256Hex(text).slice(0, 8);
97
+ controller.abort();
97
98
  return {
98
99
  cli: cliLatest,
99
100
  rtk: [rtkLocal, rtkRelease],
100
- rule: [shortHash(ruleLocalText), shortHash(ruleRemoteText)],
101
+ rule: [ruleLocalText ? sha256Hex(ruleLocalText).slice(0, 8) : null, ruleRemoteText],
101
102
  ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
102
103
  };
103
104
  }
package/cli/usage.js CHANGED
@@ -1,4 +1,6 @@
1
- import { co2GramsFor, displayModelId, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
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';
3
+ import { resolveRtkBinary } from '../extensions/lib/utils.js';
2
4
  import { readRtkGain } from '../extensions/shared/rtk-gain.js';
3
5
  import { readUsageDb, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
4
6
  import { withInteractiveSpinner } from './interactive.js';
@@ -74,6 +76,8 @@ export function summarizeUsage(rows) {
74
76
  est: usdCost({ input: r.i, output: r.o, cacheRead: r.cr ?? 0, cacheWrite: r.cw ?? 0 }, r.m).usd,
75
77
  })),
76
78
  rtkGain: readRtkGain(RTK_COMMAND_ROWS, watermark || undefined),
79
+ rtkAdoption: readRtkAdoption(),
80
+ rtkRecall: readRtkRecallDiagnostics(resolveRtkBinary()),
77
81
  usd,
78
82
  priced,
79
83
  savedUsd,
@@ -134,6 +138,7 @@ function printReport(report) {
134
138
  const t = report.tokens;
135
139
  console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`);
136
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'}`);
137
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);
138
143
  if (models.length) {
139
144
  console.log(' BY MODEL');