@krtclcdy/tersio 2.22.1 → 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.
package/README.md CHANGED
@@ -38,6 +38,8 @@ Then restart OMP and enable a preset:
38
38
 
39
39
  Individual toggles: `/caveman full` · `/rtk on` · `/ponytail full`. Everything starts off until you enable it.
40
40
 
41
+ When Tersio is installed through OMP, the first interactive launch asks for a session-start Combo preset once (`off`, `medium`, `balanced`, or `max`) and saves it as `comboDefault`. Running `tersio install` performs the same setup through the CLI.
42
+
41
43
  Session-start defaults (prompted during `install`, or flags):
42
44
 
43
45
  ```bash
@@ -61,23 +63,24 @@ Windows/WSL have separate OMP homes — install from the environment where OMP r
61
63
 
62
64
  ## 📊 Benchmarks
63
65
 
64
- Measured savings against the same workload without the modes. Token counts are real `o200k_base` BPE tokens (js-tiktoken), not chars÷4 estimates. Sample protocol, per-sample data, and caveats in [BENCHMARK.md](./BENCHMARK.md).
66
+ Measured surfaces use different boundaries. Do not treat reply-text, code-diff, and shell-output reductions as total bill savings. Full protocol and caveats are in [BENCHMARK.md](./BENCHMARK.md).
65
67
 
66
68
  | Measured surface (n) | Baseline → Tersio | Δ |
67
69
  |---|---|---|
68
- | Terse reply, caveman lite — BPE tok (3 samples, p50) | 173 → 69 tok | **−60.1%** |
69
- | Terse reply, caveman full — BPE tok (3 samples, p50) | 173 → 49 tok | **−71.7%** |
70
- | Terse reply, caveman ultra — BPE tok (3 samples, p50) | 173 → 25 tok | **−85.5%** |
71
- | Code diff, ponytail lite — BPE tok (2 tasks) | 326–462 → 57–121 tok | **−73.8…−82.5%** |
72
- | Code diff, ponytail full — BPE tok (2 tasks) | 326–462 → 55–88 tok | **−81.0…−83.1%** |
73
- | Code diff, ponytail ultra — BPE tok (2 tasks) | 326–462 → 40–88 tok | **−81.0…−87.7%** |
74
- | Shell output, rtk — `git status` — BPE tok | 182 → 72 tok | **−60.4%** |
75
- | Shell output, rtk — repo `grep` — BPE tok | 376 → 274 tok | **−27.1%** |
76
- | Shell output, rtk — `find` — BPE tok | 205 → 168 tok | **−18.0%** |
77
- | Shell output, rtk — `git diff` — BPE tok | 12,684 → 8,504 tok | **−33.0%** |
78
- | Shell output, rtk — `npm test` (passthrough) — BPE tok | 2,597 → 2,577 tok | **−0.8%** |
79
-
80
- Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn costs 941 tok baseline vs 399 with Tersio (0.42×, **−57.6% per turn**). Overhead repays in turn 1 with the bundled floor (464 tok); turn 4 with the real installed Ponytail plugin (v4.9.0, 1,264 tok). RTK concentrates savings where noise lives — hook-wired sessions record **−97.5% on summarized test suites** (rtk `history.db`, 4 runs).
70
+ | Terse reply, caveman lite — BPE tokens (3 samples, p50) | 173 → 69 | **−60.1% reply text** |
71
+ | Terse reply, caveman full — BPE tokens (3 samples, p50) | 173 → 49 | **−71.7% reply text** |
72
+ | Terse reply, caveman ultra — BPE tokens (3 samples, p50) | 173 → 25 | **−85.5% reply text** |
73
+ | Code diff, ponytail lite — BPE tokens (2 tasks) | 326–462 → 57–121 | **−73.8…−82.5% diff** |
74
+ | Code diff, ponytail full — BPE tokens (2 tasks) | 326–462 → 55–88 | **−81.0…−83.1% diff** |
75
+ | Code diff, ponytail ultra — BPE tokens (2 tasks) | 326–462 → 40–88 | **−81.0…−87.7% diff** |
76
+ | Shell output, rtk `git status` | 182 → 72 | **−60.4% command output** |
77
+ | Shell output, rtk repo `grep` | 376 → 274 | **−27.1% command output** |
78
+ | Shell output, rtk `find` | 205 → 168 | **−18.0% command output** |
79
+ | Shell output, rtk `git diff` | 12,684 → 8,504 | **−33.0% command output** |
80
+
81
+ The Command tools table reports weighted RTK command-output savings and OMP-specific adoption from executed Bash records. It does not estimate provider billing.
82
+ Upstream Ponytail's fair agentic benchmark reports 54% less code, 22% fewer tokens, 20% lower cost, and 100% retained safety. RTK estimates command-output tokens, not bill savings. Caveman and Ponytail compliance varies by model. Measure your own sessions with `tersio usage` and the dashboard.
83
+
81
84
 
82
85
  ## 📈 Dashboard
83
86
 
@@ -123,9 +126,9 @@ Mode switches live on their own commands; bare `/tersio` prints status.
123
126
  | Command | Purpose |
124
127
  |---|---|
125
128
  | `/combo off\|medium\|balanced\|max\|status` | One preset for all three (start here): medium = lite/lite/on, balanced = full/full/on, max = ultra/ultra/on; per-mode tweaks drop to `custom`. |
126
- | `/caveman lite\|full\|ultra\|wenyan\|off\|status` | Terse replies (substance stays); `ultra` uses fragments only, `wenyan` a compressed classical-Chinese-inspired register. |
127
- | `/rtk on\|off\|status` | Compact shell output via the real `rtk` binary; exact bytes (checksums, patches) bypass it by policy. |
128
- | `/ponytail off\|lite\|full\|ultra\|review\|status` | Minimal code (YAGNI): `lite` nudges, `full` enforces, `ultra` challenges the requirement, `review` audits for over-engineering (upstream command). |
129
+ | `/caveman lite\|full\|ultra\|wenyan-lite\|wenyan-full\|wenyan-ultra\|off\|status` | Terse replies with upstream-aligned Caveman levels. Legacy `wenyan` restores as `wenyan-full`. |
130
+ | `/rtk on\|off\|status` | Controls RTK session guidance, `rtk_run`, and the automatic hook through shared `RTK_DISABLED` state. Exact bytes bypass RTK. |
131
+ | `/ponytail off\|lite\|full\|ultra\|status` | Minimal code using upstream Ponytail modes. Review runs separately through `/ponytail-review`. |
129
132
  | `/tersio status` | Active modes + combo level; state persists, propagates to subagents, and lights the Combo footer indicator. |
130
133
  | `/tersio check` | Add-on version check |
131
134
  | `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | Update add-ons, preview with dry-run |
@@ -138,11 +141,11 @@ Mode switches live on their own commands; bare `/tersio` prints status.
138
141
 
139
142
  | What | Path |
140
143
  |---|---|
141
- | Caveman / RTK / Updater / Combo extensions | `~/.omp/agent/extensions/{caveman-session,rtk-session,ai-addons-updater,combo-toggle}/` |
144
+ | Caveman / RTK / Combo / Tersio commands | `~/.omp/plugins/node_modules/@krtclcdy/tersio/extensions/{caveman-session,rtk-session,combo-toggle,tersio-commands}/` — loaded from Tersio's OMP plugin manifest |
142
145
  | RTK OMP wiring (rtk-owned) | `~/.omp/agent/extensions/rtk.ts` — written by the installer via `rtk init -g --agent omp`; auto-loads, no config entry |
143
- | Ponytail package (bundled tersio dependency — one Plugins row, updates with `tersio update`) | `~/.omp/plugins/node_modules/@dietrichgebert/ponytail/` |
144
- | RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows) |
145
- | Extension registrations | `~/.omp/agent/config.yml` |
146
+ | Ponytail package (bundled Tersio dependency — one Plugins row, updates with `tersio update`) | `~/.omp/plugins/node_modules/@dietrichgebert/ponytail/` — loaded as a nested plugin dependency |
147
+ | RTK binary | Installer writes `~/.bun/bin/rtk` (`rtk.exe` on Windows). Runtime resolves `PATH` first, then this managed path. |
148
+ | Legacy config cleanup | `~/.omp/agent/config.yml` — doctor removes retired or duplicate extension entries |
146
149
 
147
150
  The installer writes `<file>.bak` before replacing an extension source; the updater keeps `rtk.bak` / `rule.md.bak` and restores them if the replacement fails validation.
148
151
 
@@ -150,7 +153,7 @@ User-level installs also register the package in `~/.omp/plugins` (visible in OM
150
153
 
151
154
  ## 🛠️ Troubleshooting
152
155
 
153
- **Ponytail or Combo command missing:** run `tersio reinstall` in OMP's environment, restart OMP, then `tersio doctor` (repairs `config.yml` registrations).
156
+ **Ponytail or Combo command missing:** confirm `~/.omp/plugins/package.json` lists `@krtclcdy/tersio`, then run `tersio reinstall`, restart OMP, and check `tersio doctor`.
154
157
 
155
158
  **RTK missing or not executable:** run `tersio reinstall`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
156
159
 
package/cli/common.js CHANGED
@@ -16,9 +16,8 @@ const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
16
16
  const PACKAGE_NAME = '@krtclcdy/tersio';
17
17
  const PACKAGE_BIN = 'tersio';
18
18
  const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('../package.json');
19
- // Session-start mode defaults for fresh installs.
20
- const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
21
- const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'review']);
19
+ const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
20
+ const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra']);
22
21
  const RTK_DEFAULTS = new Set(['on', 'off']);
23
22
  const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
24
23
  // ponytail: Combo preset implies all three modes. Mirrors COMBO_LEVELS in
@@ -167,7 +166,26 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
167
166
  const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
168
167
  lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
169
168
  }
170
- if (lines.some((l) => l.includes(normalizedPath))) {
169
+ const matchingEntries = lines.filter((entry) => entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '') === normalizedPath);
170
+ if (matchingEntries.length > 1) {
171
+ if (options.dryRun) {
172
+ if (verbose && !options.quiet)
173
+ console.log(` [dry-run] would remove ${matchingEntries.length - 1} duplicate ${label} registration${matchingEntries.length === 2 ? '' : 's'}`);
174
+ return true;
175
+ }
176
+ let kept = false;
177
+ lines = lines.filter((entry) => {
178
+ if (entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '') !== normalizedPath)
179
+ return true;
180
+ if (kept)
181
+ return false;
182
+ kept = true;
183
+ return true;
184
+ });
185
+ await writeConfigLines(configPath, lines, ` [write] Removed duplicate ${label} registration`, options);
186
+ return true;
187
+ }
188
+ if (matchingEntries.length === 1) {
171
189
  debug(`${label} already in config.yml`);
172
190
  return false;
173
191
  }
@@ -191,43 +209,29 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
191
209
  await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`, options);
192
210
  return true;
193
211
  }
194
- async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterPath, label, options = {}) {
195
- const normalizedPath = extensionPath.replace(/\\/g, '/');
196
- const normalizedAfterPath = afterPath.replace(/\\/g, '/');
197
- const line = ` - ${normalizedPath}`;
212
+ async function removeExtensionFromConfig(configPath, extensionPath, label, options = {}) {
213
+ const normalizedPath = extensionPath.replace(/\\/g, '/').replace(/^\.\//, '');
214
+ const extensionSuffix = normalizedPath.includes('/extensions/')
215
+ ? normalizedPath.slice(normalizedPath.indexOf('/extensions/') + 1)
216
+ : normalizedPath;
198
217
  const raw = await readTextIfExists(configPath);
199
- let lines = (raw || '').split('\n');
200
- // Same legacy-twin drop as ensureExtensionInConfig above.
201
- if (normalizedPath.endsWith('.ts')) {
202
- const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
203
- lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
204
- }
205
- const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
206
- const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
207
- if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
218
+ if (raw === null)
219
+ return false;
220
+ const lines = raw.split('\n');
221
+ const matches = (entry) => {
222
+ const value = entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '').replace(/\\/g, '/').replace(/^\.\//, '');
223
+ return value === extensionSuffix || value.endsWith(`/${extensionSuffix}`);
224
+ };
225
+ const kept = lines.filter((entry) => !matches(entry));
226
+ const removed = lines.length - kept.length;
227
+ if (removed === 0)
208
228
  return false;
209
229
  if (options.dryRun) {
210
230
  if (verbose && !options.quiet)
211
- console.log(` [dry-run] would place ${label} after Ponytail in config.yml`);
231
+ console.log(` [dry-run] would remove ${label} registration${removed === 1 ? '' : 's'}`);
212
232
  return true;
213
233
  }
214
- if (existingIndex !== -1)
215
- lines.splice(existingIndex, 1);
216
- const refreshedAfterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
217
- if (refreshedAfterIndex !== -1) {
218
- lines.splice(refreshedAfterIndex + 1, 0, line);
219
- }
220
- else {
221
- const extensionsIndex = lines.findIndex((entry) => EXTENSIONS_KEY_RE.test(entry));
222
- if (extensionsIndex === -1) {
223
- lines.push('extensions:', line, '');
224
- }
225
- else {
226
- normalizeExtensionsKey(lines);
227
- lines.splice(extensionsIndex + 1, 0, line);
228
- }
229
- }
230
- await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`, options);
234
+ await writeConfigLines(configPath, kept, ` [write] Removed duplicate ${label} registration`, options);
231
235
  return true;
232
236
  }
233
237
  function readPonytailConfig() {
@@ -298,4 +302,4 @@ function relTime(ageMs) {
298
302
  const weeks = Math.floor(days / 7);
299
303
  return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
300
304
  }
301
- export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
305
+ export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
package/cli/dashboard.js CHANGED
@@ -13,8 +13,10 @@ import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usa
13
13
  import { pricesCachePath } from '../extensions/shared/pricing.js';
14
14
  import { summarizeUsage } from './usage.js';
15
15
  import { isCurrencyCode } from './currency.js';
16
- import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, RTK_BINARY_NAME, } from './common.js';
16
+ import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
17
17
  import { storedProfile, writePluginSettings } from './profile.js';
18
+ import { PACKAGE_NAME } from './common.js';
19
+ import { resolveRtkBinary } from '../extensions/lib/utils.js';
18
20
  const DASHBOARD_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard', 'dist');
19
21
  const DASHBOARD_INDEX = path.join(DASHBOARD_DIST, 'index.html');
20
22
  const DASHBOARD_BRAND = path.join(DASHBOARD_DIST, 'brand.webp');
@@ -99,8 +101,8 @@ function ompDefaultModel() {
99
101
  }
100
102
  }
101
103
  function healthJson() {
102
- const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
103
- const rtkPresent = existsSync(rtkBin);
104
+ const rtkBin = resolveRtkBinary();
105
+ const rtkPresent = rtkBin !== null;
104
106
  const detectedOmpPath = ompPath();
105
107
  return JSON.stringify({
106
108
  tersio: PACKAGE_VERSION,
@@ -109,7 +111,7 @@ function healthJson() {
109
111
  omp: ompVersion(detectedOmpPath),
110
112
  ompPath: detectedOmpPath,
111
113
  provider: ompDefaultModel(),
112
- rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin },
114
+ rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin || 'not found in PATH' },
113
115
  home: tersioHomePath(),
114
116
  });
115
117
  }
@@ -184,26 +186,39 @@ function relAge(ms) {
184
186
  function computeDoctorRows() {
185
187
  const rows = [];
186
188
  const extDir = path.join(OMP_AGENT_DIR, 'extensions');
187
- const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
189
+ const rtkBin = resolveRtkBinary();
188
190
  const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
191
+ const tersioPluginDir = path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'));
189
192
  const ok = (p) => existsSync(p);
193
+ const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
190
194
  const ext = (label, p) => {
191
195
  rows.push({ label, ok: ok(p), detail: ok(p) ? 'installed' : p, group: 'Extensions' });
192
196
  };
193
197
  const addon = (label, present, detail, group = 'Add-ons') => {
194
198
  rows.push({ label, ok: present, detail, group });
195
199
  };
196
- ext('Caveman extension', path.join(extDir, 'caveman-session', 'index.ts'));
197
- ext('RTK extension', path.join(extDir, 'rtk-session', 'index.ts'));
200
+ let explicitEntries = [];
201
+ try {
202
+ explicitEntries = readFileSync(configPath, 'utf8').split('\n')
203
+ .map((line) => line.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, ''))
204
+ .filter((line) => line.startsWith('/') || line.startsWith('.'));
205
+ }
206
+ catch { /* missing config is reported by extension rows */ }
207
+ const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
208
+ addon('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : 'ok', 'Extensions');
209
+ const retiredReinforcement = explicitEntries.filter((entry) => entry.endsWith('/shared/mode-reinforcement.ts')).length;
210
+ addon('No retired reinforcement', retiredReinforcement === 0, retiredReinforcement ? `${retiredReinforcement} registration(s)` : 'ok', 'Extensions');
211
+ ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
212
+ ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
198
213
  ext('Ponytail extension', path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
199
- const rule = path.join(extDir, 'caveman-session', 'rule.md');
214
+ const rule = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'rule.md');
200
215
  const ruleAge = ageStr(rule);
201
216
  const ruleAt = absTime(rule);
202
217
  addon('Caveman rule', ruleAge !== null, ruleAge && ruleAt ? `(updated ${ruleAge} · ${ruleAt})` : rule);
203
- const rtkVer = ok(rtkBin) ? rtkVersion(rtkBin) : null;
204
- const rtkAge = ageStr(rtkBin);
205
- const rtkAt = absTime(rtkBin);
206
- addon('RTK binary', rtkVer !== null, rtkVer && rtkAge && rtkAt ? `${rtkVer} (updated ${rtkAge} · ${rtkAt})` : rtkBin);
218
+ const rtkVer = rtkBin ? rtkVersion(rtkBin) : null;
219
+ const rtkAge = rtkBin ? ageStr(rtkBin) : null;
220
+ const rtkAt = rtkBin ? absTime(rtkBin) : null;
221
+ addon('RTK binary', rtkVer !== null, rtkVer && rtkAge && rtkAt ? `${rtkVer} (updated ${rtkAge} · ${rtkAt})` : rtkBin || 'not found in PATH');
207
222
  const wiring = path.join(extDir, 'rtk.ts');
208
223
  addon('RTK OMP wiring (rtk.ts)', ok(wiring), ok(wiring) ? 'loaded' : wiring);
209
224
  let ponytailVer = null;
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' },