@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 +25 -22
- package/cli/common.js +40 -36
- package/cli/dashboard.js +27 -12
- package/cli/doctor-fix.js +35 -29
- package/cli/doctor.js +26 -15
- package/cli/install.js +13 -32
- package/cli/profile.js +1 -0
- package/cli/settings.js +1 -1
- package/cli/update.js +12 -11
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +1 -1
- package/extensions/ai-addons-updater/index.js +7 -3
- package/extensions/ai-addons-updater/index.ts +15 -3
- package/extensions/caveman-session/index.js +17 -12
- package/extensions/caveman-session/index.ts +17 -12
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +22 -2
- package/extensions/combo-toggle/index.ts +26 -2
- package/extensions/lib/utils.js +20 -4
- package/extensions/lib/utils.ts +18 -4
- package/extensions/rtk-session/index.js +11 -10
- package/extensions/rtk-session/index.ts +10 -11
- package/extensions/shared/plugin-settings.js +37 -3
- package/extensions/shared/plugin-settings.ts +34 -3
- package/extensions/shared/rtk-gain.js +2 -2
- package/extensions/shared/rtk-gain.ts +3 -3
- package/extensions/shared/session-state.js +5 -3
- package/extensions/shared/session-state.ts +4 -3
- package/extensions/shared/types.ts +5 -0
- package/extensions/shared/usage-ledger.js +113 -3
- package/extensions/shared/usage-ledger.ts +121 -3
- package/package.json +12 -6
- package/tersio.js +2 -2
- package/extensions/shared/mode-reinforcement.ts +0 -29
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
|
|
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
|
|
69
|
-
| Terse reply, caveman full — BPE
|
|
70
|
-
| Terse reply, caveman ultra — BPE
|
|
71
|
-
| Code diff, ponytail lite — BPE
|
|
72
|
-
| Code diff, ponytail full — BPE
|
|
73
|
-
| Code diff, ponytail ultra — BPE
|
|
74
|
-
| Shell output, rtk
|
|
75
|
-
| Shell output, rtk
|
|
76
|
-
| Shell output, rtk
|
|
77
|
-
| Shell output, rtk
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
127
|
-
| `/rtk on\|off\|status` |
|
|
128
|
-
| `/ponytail off\|lite\|full\|ultra\|
|
|
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 /
|
|
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
|
|
144
|
-
| RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows) |
|
|
145
|
-
|
|
|
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:**
|
|
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
|
-
|
|
20
|
-
const
|
|
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
|
-
|
|
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
|
|
195
|
-
const normalizedPath = extensionPath.replace(/\\/g, '/');
|
|
196
|
-
const
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
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
|
|
231
|
+
console.log(` [dry-run] would remove ${label} registration${removed === 1 ? '' : 's'}`);
|
|
212
232
|
return true;
|
|
213
233
|
}
|
|
214
|
-
|
|
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,
|
|
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 {
|
|
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 =
|
|
103
|
-
const rtkPresent =
|
|
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 =
|
|
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
|
-
|
|
197
|
-
|
|
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(
|
|
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 =
|
|
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,
|
|
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(
|
|
27
|
-
console.log(' Doctor --fix: restoring extension files');
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
for (
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
+
await writeIfChanged(path.join(pluginExtDir, to), text, { dryRun, verbose });
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
if ((await readTextIfExists(
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
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(
|
|
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 =
|
|
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',
|
|
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
|
|
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: '
|
|
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' },
|