@krtclcdy/tersio 2.24.1 → 2.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -21
- package/cli/banner.js +7 -4
- package/cli/common.js +14 -13
- package/cli/currency.js +1 -2
- package/cli/dashboard.js +57 -14
- package/cli/doctor-fix.js +88 -21
- package/cli/doctor.js +32 -24
- package/cli/hosts.js +19 -18
- package/cli/install.js +123 -52
- package/cli/interactive.js +29 -19
- package/cli/manifest.js +7 -0
- package/cli/profile.js +42 -11
- package/cli/rtk-wiring.js +28 -26
- package/cli/settings.js +37 -8
- package/cli/uninstall.js +40 -7
- package/cli/update.js +22 -14
- package/cli/usage.js +39 -14
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +69 -15
- package/extensions/ai-addons-updater/index.ts +60 -17
- package/extensions/caveman-session/index.js +28 -55
- package/extensions/caveman-session/index.ts +28 -55
- package/extensions/combo-toggle/index.js +40 -58
- package/extensions/combo-toggle/index.ts +38 -67
- package/extensions/lib/utils.js +60 -15
- package/extensions/lib/utils.ts +59 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +20 -35
- package/extensions/rtk-session/index.ts +19 -34
- package/extensions/shared/carbon.js +1 -2
- package/extensions/shared/carbon.ts +1 -2
- package/extensions/shared/host.js +95 -46
- package/extensions/shared/host.ts +106 -50
- package/extensions/shared/plugin-settings.js +4 -6
- package/extensions/shared/plugin-settings.ts +4 -6
- package/extensions/shared/pricing.js +79 -25
- package/extensions/shared/pricing.ts +80 -26
- package/extensions/shared/rtk-gain.js +3 -5
- package/extensions/shared/rtk-gain.ts +3 -5
- package/extensions/shared/session-state.js +10 -33
- package/extensions/shared/session-state.ts +12 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +190 -56
- package/extensions/shared/usage-ledger.ts +194 -58
- package/extensions/shared/usage-store.js +228 -64
- package/extensions/shared/usage-store.ts +235 -67
- package/extensions/tersio-commands/index.js +4 -9
- package/extensions/tersio-commands/index.ts +4 -9
- package/package.json +8 -2
- package/tersio.js +3 -3
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -22
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
package/README.md
CHANGED
|
@@ -23,29 +23,23 @@ curl -fsSL https://github.com/KurutoDenzeru/tersio/releases/latest/download/inst
|
|
|
23
23
|
Then run the CLI. Bare `tersio` opens a menu and asks which agent to install into:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
tersio # menu — picks Oh My Pi or
|
|
26
|
+
tersio # menu — picks Oh My Pi, pi, or OpenCode
|
|
27
27
|
tersio install --host pi # pi, non-interactive
|
|
28
28
|
tersio install --host omp # Oh My Pi, non-interactive
|
|
29
|
+
tersio install --host opencode # OpenCode, non-interactive
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
Or let
|
|
32
|
+
Or let each host install the package itself:
|
|
32
33
|
|
|
33
34
|
```bash
|
|
34
35
|
omp plugin install @krtclcdy/tersio
|
|
35
36
|
pi install npm:@krtclcdy/tersio
|
|
37
|
+
opencode plugin add @krtclcdy/tersio
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
Pick one install method per host, not both. Using both registers every command twice.
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```text
|
|
43
|
-
/combo balanced
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Individual toggles: `/caveman full` · `/rtk on` · `/ponytail full`. Everything starts off until you enable it.
|
|
47
|
-
|
|
48
|
-
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
|
+
All hosts load the same five extensions from the same sources and read their session-start defaults from `~/.tersio/settings.json`. All five load always.
|
|
49
43
|
|
|
50
44
|
One-off use without installing:
|
|
51
45
|
|
|
@@ -55,14 +49,15 @@ npm exec --yes --prefer-online --package=@krtclcdy/tersio@latest -- tersio insta
|
|
|
55
49
|
|
|
56
50
|
### Requirements
|
|
57
51
|
|
|
58
|
-
- [OMP](https://github.com/can1357/oh-my-pi)
|
|
52
|
+
- [OMP](https://github.com/can1357/oh-my-pi), [pi](https://github.com/earendil-works/pi), or [OpenCode](https://opencode.ai) — all three are supported; pick one with `tersio install --host <omp|pi|opencode>`
|
|
59
53
|
- Node.js 20.12+ with npm
|
|
54
|
+
- macOS or Linux, native or WSL
|
|
60
55
|
|
|
61
|
-
Windows
|
|
56
|
+
On Windows, use WSL. There is no native Windows path: the installer, the extensions, and the RTK wiring are tested on macOS and Linux only. WSL has its own home directory, so install from inside WSL — `command -v npm` must resolve to a Linux path, not `/mnt/c/`.
|
|
62
57
|
|
|
63
58
|
## 📊 Benchmarks
|
|
64
59
|
|
|
65
|
-
Every mode is measured against a base run with all modes off. Measured surfaces use different boundaries. Do not treat reply-text, code-output, and shell-output reductions as total bill savings.
|
|
60
|
+
Every mode is measured against a base run with all modes off. Measured surfaces use different boundaries. Do not treat reply-text, code-output, and shell-output reductions as total bill savings. **A mode also spends tokens on the prompt, on every turn — read the caveat under the table before choosing.** Results and the rerun protocol are in [docs/BENCHMARK.md](./docs/BENCHMARK.md).
|
|
66
61
|
|
|
67
62
|
| Mode | Surface (n) | Base → Tersio | Δ |
|
|
68
63
|
|---|---|---|---|
|
|
@@ -78,7 +73,9 @@ Every mode is measured against a base run with all modes off. Measured surfaces
|
|
|
78
73
|
| `/rtk on` — `git status` | Command output (3 runs, p50) | 72 → 18 | **−75.0% output** |
|
|
79
74
|
| `/rtk on` — `bun run build` | Command output (3 runs, p50) | 140 → 114 | **−18.6% output** |
|
|
80
75
|
|
|
81
|
-
|
|
76
|
+
The two `/caveman` rows above are the exception worth naming: both levels spend more prompt tokens than the reply text they save, so they cost you money on net. Use Caveman for the terser writing style, not as a token saving.
|
|
77
|
+
|
|
78
|
+
Prompt overhead is included in [docs/BENCHMARK.md](./docs/BENCHMARK.md) with a full prompt-base example. Summary: `/rtk on` pays back immediately, `/ponytail ultra` repays in about 7 code tasks, `/combo medium` in about 8, and every Caveman level costs more in prompt than it saves in reply text.
|
|
82
79
|
|
|
83
80
|
The Command tools dashboard reports weighted RTK command-output savings and OMP-specific adoption from executed Bash records. It does not estimate provider billing.
|
|
84
81
|
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.
|
|
@@ -121,7 +118,13 @@ Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-defa
|
|
|
121
118
|
|
|
122
119
|
## ⌨️ Commands reference
|
|
123
120
|
|
|
124
|
-
Mode switches live on their own commands; bare `/tersio` prints status.
|
|
121
|
+
Mode switches live on their own commands; bare `/tersio` prints status. Every mode command, and every session start, prints the status line:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
🧩 combo MAX: 🪨caveman=ULTRA ⚡rtk=ON 🦥ponytail=ULTRA
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It is display-only — it never enters the model context, so it costs the session nothing.
|
|
125
128
|
|
|
126
129
|
| Command | Purpose |
|
|
127
130
|
|---|---|
|
|
@@ -129,7 +132,7 @@ Mode switches live on their own commands; bare `/tersio` prints status.
|
|
|
129
132
|
| `/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
133
|
| `/rtk on\|off\|status` | Controls RTK session guidance, `rtk_run`, and the automatic hook through shared `RTK_DISABLED` state. Exact bytes bypass RTK. |
|
|
131
134
|
| `/ponytail off\|lite\|full\|ultra\|status` | Minimal code using upstream Ponytail modes. Review runs separately through `/ponytail-review`. |
|
|
132
|
-
| `/tersio status` | Active modes + combo level; state persists
|
|
135
|
+
| `/tersio status` | Active modes + combo level; state persists and propagates to subagents. |
|
|
133
136
|
| `/tersio check` | Add-on version check |
|
|
134
137
|
| `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | Update add-ons, preview with dry-run |
|
|
135
138
|
| `/tersio dashboard` | Open the Dashboard. |
|
|
@@ -147,14 +150,15 @@ Shared, and identical for every host:
|
|
|
147
150
|
| RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows). The runtime resolves `PATH` first, then this managed path. |
|
|
148
151
|
| Usage ledger and price cache | `~/.tersio/` — local only, nothing leaves the machine |
|
|
149
152
|
|
|
150
|
-
Per host, written by `tersio install --host <omp|pi>`:
|
|
153
|
+
Per host, written by `tersio install --host <omp|pi|opencode>`:
|
|
151
154
|
|
|
152
155
|
| Host | Extensions | Ponytail |
|
|
153
156
|
|---|---|---|
|
|
154
157
|
| Oh My Pi | `~/.omp/agent/extensions/{caveman-session,rtk-session,combo-toggle,tersio-commands,ai-addons-updater}/` | nested plugin dependency under `~/.omp/plugins/node_modules/`, one Plugins row |
|
|
155
158
|
| pi | `~/.pi/agent/extensions/{caveman-session,rtk-session,combo-toggle,tersio-commands,ai-addons-updater}/` — pi auto-discovers `<agent-dir>/extensions`, so there is nothing to register | `npm:@dietrichgebert/ponytail`, installed as a pi package |
|
|
159
|
+
| OpenCode | `~/.config/opencode/plugins/tersio/` — registered in `opencode.json` plugins, so there is nothing else to register | bundled with Tersio; no separate package |
|
|
156
160
|
|
|
157
|
-
|
|
161
|
+
All three hosts carry the same `shared/` and `lib/` modules and the same mode extensions from the same sources.
|
|
158
162
|
|
|
159
163
|
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. `tersio doctor --fix extensions` restores a damaged tree, and `doctor --fix registrations` drops retired or duplicate `config.yml` entries.
|
|
160
164
|
|
|
@@ -166,6 +170,8 @@ An OMP install also registers the package in `~/.omp/plugins`, so it shows under
|
|
|
166
170
|
|
|
167
171
|
**Commands missing on pi:** check the `Hosts` section of `tersio doctor` — it reports whether pi is installed and where. Then run `tersio install --host pi` and restart pi.
|
|
168
172
|
|
|
173
|
+
**Commands missing on OpenCode:** run `tersio install --host opencode` and restart OpenCode.
|
|
174
|
+
|
|
169
175
|
**RTK missing or not executable:** run `tersio install`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
|
|
170
176
|
|
|
171
177
|
**RTK commands not metered in the Dashboard:** only bash calls routed through rtk's OMP wiring (`~/.omp/agent/extensions/rtk.ts`, written by `rtk init -g --agent omp`) are metered. Missing: run that command (needs rtk ≥ 0.49) or `tersio doctor --fix rtk`, then restart OMP. Native tool calls (`read`/`edit`/`eval`) stay unmetered.
|
|
@@ -176,7 +182,7 @@ An OMP install also registers the package in `~/.omp/plugins`, so it shows under
|
|
|
176
182
|
|
|
177
183
|
Contributions are always welcome, whether you're fixing bugs, improving docs, or shipping new features that make the project better for everyone.
|
|
178
184
|
|
|
179
|
-
Check out [
|
|
185
|
+
Check out [CONTRIBUTING.md](CONTRIBUTING.md) to learn how to get started and follow the recommended workflow.
|
|
180
186
|
|
|
181
187
|
## ⚖️ License
|
|
182
188
|
|
package/cli/banner.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// cli/banner.ts — mini pixel-scissor welcome. TTY-only and skipped on
|
|
2
|
-
// --dry-run, so piped output stays byte-identical.
|
|
1
|
+
// cli/banner.ts — mini pixel-scissor welcome. TTY-only and skipped on --dry-run, so piped output stays byte-identical.
|
|
3
2
|
import { intro as clackIntro, note as clackNote } from '@clack/prompts';
|
|
4
3
|
import { PACKAGE_VERSION, dryRun } from './common.js';
|
|
5
4
|
function tty() {
|
|
@@ -71,8 +70,7 @@ function bannerLines(tier = bannerTier()) {
|
|
|
71
70
|
return out;
|
|
72
71
|
}
|
|
73
72
|
let shown = false;
|
|
74
|
-
//
|
|
75
|
-
// No-op unless interactive (and never on --dry-run).
|
|
73
|
+
// Interactive only, so piped and --dry-run output stays byte-identical.
|
|
76
74
|
function printWelcome() {
|
|
77
75
|
if (shown || dryRun || !tty())
|
|
78
76
|
return;
|
|
@@ -83,6 +81,11 @@ function printWelcome() {
|
|
|
83
81
|
'tersio update refresh CLI + add-ons',
|
|
84
82
|
'tersio doctor verify the install',
|
|
85
83
|
];
|
|
84
|
+
// Below 26 rows the note scrolls off the top and slices the art.
|
|
85
|
+
if (process.stdout.rows < 26) {
|
|
86
|
+
clackNote(tips.map((t) => ` ${t}`).join('\n'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
86
89
|
const art = bannerLines();
|
|
87
90
|
const rows = art.map((line, i) => (i >= 1 && i <= 3 ? `${line} ${tips[i - 1]}` : line));
|
|
88
91
|
clackNote(rows.join('\n'));
|
package/cli/common.js
CHANGED
|
@@ -20,8 +20,7 @@ const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan-lite',
|
|
|
20
20
|
const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra']);
|
|
21
21
|
const RTK_DEFAULTS = new Set(['on', 'off']);
|
|
22
22
|
const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
23
|
-
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts (rtk as a
|
|
24
|
-
// boolean here), so the combo preset implies all three modes.
|
|
23
|
+
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts (rtk as a boolean here), so the combo preset implies all three modes.
|
|
25
24
|
const COMBO_PRESET_MODES = {
|
|
26
25
|
off: { caveman: 'off', rtk: false, ponytail: 'off' },
|
|
27
26
|
medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
|
|
@@ -51,8 +50,7 @@ const COMMANDS = { install: true, update: true, doctor: true, uninstall: true, u
|
|
|
51
50
|
const args = process.argv.slice(2);
|
|
52
51
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
53
52
|
const command = commandArg?.toLowerCase() || null;
|
|
54
|
-
// `tersio settings diagnosis` jumps to one prompt instead of walking the
|
|
55
|
-
// chain. Index-based, so flag values never count as positionals.
|
|
53
|
+
// `tersio settings diagnosis` jumps to one prompt instead of walking the chain. Index-based, so flag values never count as positionals.
|
|
56
54
|
const settingArg = (() => {
|
|
57
55
|
const at = args.indexOf(commandArg ?? '');
|
|
58
56
|
const next = at >= 0 ? args[at + 1] : undefined;
|
|
@@ -69,6 +67,8 @@ const applyUpdate = args.includes('--apply-update');
|
|
|
69
67
|
const dryRun = args.includes('--dry-run');
|
|
70
68
|
const yes = args.includes('--yes') || args.includes('-y') || install || update || applyUpdate;
|
|
71
69
|
const verbose = args.includes('--verbose');
|
|
70
|
+
// Off by default: an unverified RTK binary runs with the user's privileges.
|
|
71
|
+
const allowUnverified = args.includes('--allow-unverified');
|
|
72
72
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
73
73
|
const fix = args.includes('--fix') || args.some((a) => a.startsWith('--fix='));
|
|
74
74
|
const usage = command === 'usage' || args.includes('--usage');
|
|
@@ -85,8 +85,7 @@ const uninstall = command === 'uninstall' || args.includes('--uninstall');
|
|
|
85
85
|
const removePonytail = args.includes('--remove-ponytail');
|
|
86
86
|
const keepPonytail = args.includes('--keep-ponytail');
|
|
87
87
|
const removeRtk = args.includes('--remove-rtk');
|
|
88
|
-
// User scope only (all OMP sessions). A bare `--scope user` still parses for
|
|
89
|
-
// old scripts; anything else fails.
|
|
88
|
+
// User scope only (all OMP sessions). A bare `--scope user` still parses for old scripts; anything else fails.
|
|
90
89
|
const legacyScope = flagValue('--scope')?.toLowerCase() ?? null;
|
|
91
90
|
if (legacyScope !== null && legacyScope !== 'user') {
|
|
92
91
|
console.error(`[fail] Invalid --scope: ${legacyScope}. Project scope was removed; tersio installs user-level only.`);
|
|
@@ -100,6 +99,11 @@ const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rt
|
|
|
100
99
|
const diagScheduleFlag = parseEnum(flagValue('--diag-schedule'), DIAG_SCHEDULES, '--diag-schedule');
|
|
101
100
|
const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
|
|
102
101
|
.some((flag) => flag !== undefined);
|
|
102
|
+
// Repeatable `--subagent-marker`; overrides the built-in host prompt marker.
|
|
103
|
+
const subagentMarkerFlags = args
|
|
104
|
+
.map((a, i) => (a === '--subagent-marker' ? args[i + 1] : undefined))
|
|
105
|
+
.filter((v) => typeof v === 'string' && !v.startsWith('-') && v.trim() !== '');
|
|
106
|
+
const subagentMarkersGiven = subagentMarkerFlags.length > 0;
|
|
103
107
|
function debug(...a) {
|
|
104
108
|
if (verbose)
|
|
105
109
|
console.log(' [debug]', ...a);
|
|
@@ -133,8 +137,7 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
133
137
|
console.log(` [write] ${dest}`);
|
|
134
138
|
return true;
|
|
135
139
|
}
|
|
136
|
-
// omp ships 'extensions: null', and list items under a null scalar break YAML:
|
|
137
|
-
// normalize null/~/[]/empty to a mapping key first.
|
|
140
|
+
// omp ships 'extensions: null', and list items under a null scalar break YAML: normalize null/~/[]/empty to a mapping key first.
|
|
138
141
|
const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
|
|
139
142
|
const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
|
|
140
143
|
function normalizeExtensionsKey(lines) {
|
|
@@ -156,8 +159,7 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
156
159
|
const line = ` - ${normalizedPath}`;
|
|
157
160
|
const raw = await readTextIfExists(configPath);
|
|
158
161
|
let lines = (raw || '').split('\n');
|
|
159
|
-
// Drop legacy compiled twins (same dir/name, .js): OMP must never load both
|
|
160
|
-
// copies. Matched on the trailing path so relative entries are caught too.
|
|
162
|
+
// Drop legacy compiled twins (same dir/name, .js): OMP must never load both copies. Matched on the trailing path so relative entries are caught too.
|
|
161
163
|
if (normalizedPath.endsWith('.ts')) {
|
|
162
164
|
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
163
165
|
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
@@ -277,8 +279,7 @@ async function ensurePonytailConfigValue(key, value, options = {}) {
|
|
|
277
279
|
return true;
|
|
278
280
|
}, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
|
|
279
281
|
}
|
|
280
|
-
// Read plugins/package.json tolerantly; a corrupt or missing file starts
|
|
281
|
-
// fresh with installer-managed defaults.
|
|
282
|
+
// Read plugins/package.json tolerantly; a corrupt or missing file starts fresh with installer-managed defaults.
|
|
282
283
|
async function readPluginsPackage(pkgPath) {
|
|
283
284
|
const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
|
|
284
285
|
return { ...pkg, name: pkg.name || 'omp-plugins', private: true, dependencies: pkg.dependencies || {} };
|
|
@@ -298,4 +299,4 @@ function relTime(ageMs) {
|
|
|
298
299
|
const weeks = Math.floor(days / 7);
|
|
299
300
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
300
301
|
}
|
|
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, 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, };
|
|
302
|
+
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, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
|
package/cli/currency.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// cli/currency.ts — USD-base currency helpers on offline snapshot rates. Same
|
|
2
|
-
// 10 currencies as the dashboard, which prefers live frankfurter figures.
|
|
1
|
+
// cli/currency.ts — USD-base currency helpers on offline snapshot rates. Same 10 currencies as the dashboard, which prefers live frankfurter figures.
|
|
3
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
3
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
5
4
|
const CURRENCY_CODES = [
|
package/cli/dashboard.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Serves the built Dashboard with local usage APIs. Binds 127.0.0.1 only;
|
|
2
|
-
// --export writes a file instead. No Promise.withResolvers: Node 20.12 lacks it.
|
|
1
|
+
// Serves the built Dashboard with local usage APIs. Binds 127.0.0.1 only; --export writes a file instead. No Promise.withResolvers: Node 20.12 lacks it.
|
|
3
2
|
import { spawn } from 'node:child_process';
|
|
4
3
|
import { execFileSync } from 'node:child_process';
|
|
5
4
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -10,10 +9,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
9
|
import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
11
10
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
12
11
|
import { summarizeUsage } from './usage.js';
|
|
13
|
-
import { deleteUsageBackup, listUsageBackups, restoreUsageBackup } from '../extensions/shared/usage-store.js';
|
|
12
|
+
import { backupUsageDb, deleteUsageBackup, listUsageBackups, restoreUsageBackup, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
14
13
|
import { isCurrencyCode } from './currency.js';
|
|
15
14
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
16
|
-
import { BACKUP_SCHEDULES, storedProfile, writePluginSettings } from './profile.js';
|
|
15
|
+
import { BACKUP_SCHEDULES, formatCliStatus, storedProfile, writePluginSettings } from './profile.js';
|
|
17
16
|
import { normalizeComboLevel } from '../extensions/shared/session-state.js';
|
|
18
17
|
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS } from './common.js';
|
|
19
18
|
import { PACKAGE_NAME } from './common.js';
|
|
@@ -53,6 +52,17 @@ async function brandDataUri() {
|
|
|
53
52
|
function dataJson() {
|
|
54
53
|
return JSON.stringify(summarizeUsage(readUsage()));
|
|
55
54
|
}
|
|
55
|
+
// Runs outside any agent session, so it reports the persisted defaults.
|
|
56
|
+
async function statusJson() {
|
|
57
|
+
const profile = await storedProfile();
|
|
58
|
+
return JSON.stringify({
|
|
59
|
+
status: formatCliStatus(profile),
|
|
60
|
+
level: profile.comboDefault,
|
|
61
|
+
caveman: profile.cavemanDefault,
|
|
62
|
+
rtk: profile.rtkDefault ? 'on' : 'off',
|
|
63
|
+
ponytail: profile.ponytailDefault,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
56
66
|
// Local-only health: find each agent on PATH, run `--version`, report both.
|
|
57
67
|
function binOnPath(bin) {
|
|
58
68
|
const names = process.platform === 'win32' ? [`${bin}.cmd`, `${bin}.exe`, `${bin}.bat`, bin] : [bin];
|
|
@@ -74,6 +84,9 @@ function ompPath() {
|
|
|
74
84
|
function piPath() {
|
|
75
85
|
return binOnPath('pi');
|
|
76
86
|
}
|
|
87
|
+
function opencodePath() {
|
|
88
|
+
return binOnPath('opencode');
|
|
89
|
+
}
|
|
77
90
|
// omp prints `omp/18.4.1`, pi prints a bare `0.87.1`; both read as name/version.
|
|
78
91
|
function versionLabel(bin, raw) {
|
|
79
92
|
const out = raw.trim();
|
|
@@ -118,14 +131,27 @@ function healthJson() {
|
|
|
118
131
|
const rtkPresent = rtkBin !== null;
|
|
119
132
|
const detectedOmpPath = ompPath();
|
|
120
133
|
const detectedPiPath = piPath();
|
|
134
|
+
const detectedOpencodePath = opencodePath();
|
|
135
|
+
const agentDir = (id) => {
|
|
136
|
+
if (id === 'omp')
|
|
137
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.omp');
|
|
138
|
+
if (id === 'pi')
|
|
139
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.pi');
|
|
140
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.config', 'opencode');
|
|
141
|
+
};
|
|
121
142
|
return JSON.stringify({
|
|
122
143
|
tersio: PACKAGE_VERSION,
|
|
123
144
|
node: process.version,
|
|
124
145
|
platform: `${process.platform}/${process.arch}`,
|
|
125
146
|
omp: agentVersion(detectedOmpPath),
|
|
126
147
|
ompPath: detectedOmpPath,
|
|
148
|
+
ompDir: agentDir('omp'),
|
|
127
149
|
pi: agentVersion(detectedPiPath),
|
|
128
150
|
piPath: detectedPiPath,
|
|
151
|
+
piDir: agentDir('pi'),
|
|
152
|
+
opencode: agentVersion(detectedOpencodePath),
|
|
153
|
+
opencodePath: detectedOpencodePath,
|
|
154
|
+
opencodeDir: agentDir('opencode'),
|
|
129
155
|
provider: ompDefaultModel(),
|
|
130
156
|
rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin || 'not found in PATH' },
|
|
131
157
|
home: tersioHomePath(),
|
|
@@ -219,16 +245,14 @@ function computeDoctorRows() {
|
|
|
219
245
|
}
|
|
220
246
|
catch { /* missing config is reported by extension rows */ }
|
|
221
247
|
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
222
|
-
addon('Unique config registrations',
|
|
223
|
-
// A listed extension whose file is gone makes the host warn on every load, so
|
|
224
|
-
// surface it rather than leaving the user with only a startup message.
|
|
248
|
+
addon('Unique config registrations', true, 'ok', 'Extensions');
|
|
249
|
+
// A listed extension whose file is gone makes the host warn on every load, so surface it rather than leaving the user with only a startup message.
|
|
225
250
|
const dangling = explicitEntries
|
|
226
251
|
.filter((entry) => entry.includes(`extensions${path.sep}`) || entry.includes('extensions/'))
|
|
227
252
|
.filter((entry) => !ok(path.isAbsolute(entry) ? entry : path.resolve(OMP_AGENT_DIR, entry)));
|
|
228
253
|
addon('Extension files present', dangling.length === 0, dangling.length ? dangling.join(', ') : 'ok', 'Extensions');
|
|
229
254
|
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
|
|
230
255
|
ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
|
|
231
|
-
ext('Ponytail extension', path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
|
|
232
256
|
const rule = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'rule.md');
|
|
233
257
|
const ruleAge = ageStr(rule);
|
|
234
258
|
const ruleAt = absTime(rule);
|
|
@@ -253,8 +277,7 @@ function computeDoctorRows() {
|
|
|
253
277
|
addon('Prices feed', pricesAge !== null, pricesAge && pricesAt ? `(pulled ${pricesAge} · ${pricesAt})` : prices, 'Usage');
|
|
254
278
|
return rows;
|
|
255
279
|
}
|
|
256
|
-
// Recompute when forced, missing, or past the schedule; a report holding a
|
|
257
|
-
// retired label is stale by definition.
|
|
280
|
+
// Recompute when forced, missing, or past the schedule; a report holding a retired label is stale by definition.
|
|
258
281
|
const RETIRED_DIAG_LABELS = new Set([
|
|
259
282
|
'OMP CLI',
|
|
260
283
|
'Tersio CLI',
|
|
@@ -264,8 +287,13 @@ const RETIRED_DIAG_LABELS = new Set([
|
|
|
264
287
|
'Combo extension',
|
|
265
288
|
'Self plugin',
|
|
266
289
|
'Ponytail registered',
|
|
290
|
+
'Ponytail extension',
|
|
267
291
|
'Usage store',
|
|
268
292
|
'RTK OMP wiring (rtk.ts)',
|
|
293
|
+
'OpenCode plugin entry',
|
|
294
|
+
'OpenCode RTK plugin',
|
|
295
|
+
'pi RTK extension',
|
|
296
|
+
'No foreign opencode trees',
|
|
269
297
|
]);
|
|
270
298
|
function isRetiredReport(report) {
|
|
271
299
|
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
@@ -334,8 +362,7 @@ async function saveDefaults(body) {
|
|
|
334
362
|
function readDiagSchedule() {
|
|
335
363
|
return readDiagReport()?.schedule ?? 'manual';
|
|
336
364
|
}
|
|
337
|
-
// Persist the picker choice: every run serves a fresh port, so localStorage
|
|
338
|
-
// alone cannot survive a restart.
|
|
365
|
+
// Persist the picker choice: every run serves a fresh port, so localStorage alone cannot survive a restart.
|
|
339
366
|
async function saveDashboardCurrency(raw) {
|
|
340
367
|
if (typeof raw !== 'string')
|
|
341
368
|
return null;
|
|
@@ -403,8 +430,7 @@ function openBrowser(url) {
|
|
|
403
430
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
404
431
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
405
432
|
}
|
|
406
|
-
// Replacers throughout: session data holds `$'` (shell quoting) and literal
|
|
407
|
-
// `</script>`, which String.replace would mangle.
|
|
433
|
+
// Replacers throughout: session data holds `$'` (shell quoting) and literal `</script>`, which String.replace would mangle.
|
|
408
434
|
function escapeInline(json) {
|
|
409
435
|
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
410
436
|
}
|
|
@@ -453,6 +479,18 @@ async function runDashboard(options) {
|
|
|
453
479
|
res.end(JSON.stringify({ backups: rows }));
|
|
454
480
|
return;
|
|
455
481
|
}
|
|
482
|
+
if (req.url === '/backups/create' && req.method === 'POST') {
|
|
483
|
+
try {
|
|
484
|
+
backupUsageDb(usageDbPath());
|
|
485
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
486
|
+
res.end(JSON.stringify({ ok: true }));
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
490
|
+
res.end(JSON.stringify({ ok: false, error: 'backup failed' }));
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
456
494
|
if (req.url === '/backups/delete' && req.method === 'POST') {
|
|
457
495
|
let file = '';
|
|
458
496
|
try {
|
|
@@ -544,6 +582,11 @@ async function runDashboard(options) {
|
|
|
544
582
|
res.end(healthJson());
|
|
545
583
|
return;
|
|
546
584
|
}
|
|
585
|
+
if (req.url === '/status' && req.method === 'GET') {
|
|
586
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
587
|
+
res.end(await statusJson());
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
547
590
|
if (req.url === '/doctor' && req.method === 'POST') {
|
|
548
591
|
let schedule = 'manual';
|
|
549
592
|
try {
|
package/cli/doctor-fix.js
CHANGED
|
@@ -4,24 +4,38 @@ import path from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
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, sayTagged } from './interactive.js';
|
|
7
|
-
import { wireRtkOmp, ensureRtkInConfig } from './rtk-wiring.js';
|
|
7
|
+
import { wireRtkOmp, wireRtkOpencode, wireRtkPi, 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';
|
|
9
9
|
import { runLatestUpdate } from './update.js';
|
|
10
|
-
import { TREE_FILES, sourcePath } from './manifest.js';
|
|
10
|
+
import { OPENCODE_SERVER_SHIM, TREE_FILES, sourcePath } from './manifest.js';
|
|
11
11
|
import { detectHosts, hostExtensionsDir } from './hosts.js';
|
|
12
12
|
const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
|
13
|
-
// A written tree is the install for both hosts now, so --fix restores it for
|
|
14
|
-
// whichever hosts have it, alongside the OMP package when that is present.
|
|
13
|
+
// A written tree is the install for both hosts now, so --fix restores it for whichever hosts have it, alongside the OMP package when that is present.
|
|
15
14
|
async function fixExtensionTrees() {
|
|
15
|
+
// The opencode entrypoint is foreign to pi/omp loaders: never copy it there, and remove strays.
|
|
16
|
+
const forHost = (id) => TREE_FILES.filter((file) => id === 'opencode' || !file.startsWith('opencode/'));
|
|
17
|
+
const scrubOpencodeDir = async (dest) => {
|
|
18
|
+
const dir = path.join(dest, 'opencode');
|
|
19
|
+
if (!existsSync(dir))
|
|
20
|
+
return;
|
|
21
|
+
if (dryRun) {
|
|
22
|
+
sayTagged(` [dry-run] would remove foreign ${dir}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
26
|
+
sayTagged(` [rm] foreign ${dir}`);
|
|
27
|
+
};
|
|
16
28
|
for (const host of detectHosts()) {
|
|
17
29
|
const dest = hostExtensionsDir(host.id);
|
|
18
|
-
// Repair a host whose tree is partly there: a half-written tree is exactly
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
// Repair a host whose tree is partly there: a half-written tree is exactly the case this runs for, and it reads as "not installed" to the detector.
|
|
31
|
+
const present = forHost(host.id).filter((file) => existsSync(path.join(dest, file))).length;
|
|
32
|
+
if (present === 0) {
|
|
33
|
+
if (host.id !== 'opencode')
|
|
34
|
+
await scrubOpencodeDir(dest);
|
|
22
35
|
continue;
|
|
36
|
+
}
|
|
23
37
|
await fs.mkdir(dest, { recursive: true });
|
|
24
|
-
for (const file of
|
|
38
|
+
for (const file of forHost(host.id)) {
|
|
25
39
|
const from = path.join(EXT_DIR, ...file.split('/'));
|
|
26
40
|
const text = await readTextIfExists(from);
|
|
27
41
|
if (text === null)
|
|
@@ -29,26 +43,59 @@ async function fixExtensionTrees() {
|
|
|
29
43
|
else
|
|
30
44
|
await writeIfChanged(path.join(dest, file), text, { dryRun, verbose });
|
|
31
45
|
}
|
|
46
|
+
if (host.id !== 'opencode')
|
|
47
|
+
await scrubOpencodeDir(dest);
|
|
32
48
|
if (!dryRun)
|
|
33
49
|
sayTagged(` [ok] ${host.label} extension tree: ${dest}`);
|
|
34
50
|
}
|
|
35
51
|
}
|
|
52
|
+
// The tree alone does nothing on OpenCode without its entrypoint registered in opencode.json.
|
|
53
|
+
async function fixOpencodeEntry() {
|
|
54
|
+
const { HOME } = await import('./common.js');
|
|
55
|
+
const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
56
|
+
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
57
|
+
const nested = path.join(dir, 'opencode', 'server.ts');
|
|
58
|
+
const nestedText = await readTextIfExists(nested);
|
|
59
|
+
if (!nestedText)
|
|
60
|
+
return;
|
|
61
|
+
// The loader resolves <pluginDir>/server.ts, so mirror the entry at the tree root.
|
|
62
|
+
await writeIfChanged(path.join(dir, 'server.ts'), OPENCODE_SERVER_SHIM, { dryRun, verbose });
|
|
63
|
+
const raw = await readTextIfExists(configPath);
|
|
64
|
+
let config = {};
|
|
65
|
+
try {
|
|
66
|
+
config = raw ? JSON.parse(raw) : {};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const plugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
|
|
72
|
+
const withoutTersio = plugins.filter((p) => typeof p !== 'string' || !p.includes('/opencode/plugins/tersio'));
|
|
73
|
+
if (withoutTersio.includes(dir))
|
|
74
|
+
return;
|
|
75
|
+
withoutTersio.push(dir);
|
|
76
|
+
config.plugins = withoutTersio;
|
|
77
|
+
if (dryRun) {
|
|
78
|
+
sayTagged(` [dry-run] would register tersio in ${configPath}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
82
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
83
|
+
sayTagged(` [ok] OpenCode plugin entry: ${configPath}`);
|
|
84
|
+
}
|
|
36
85
|
async function fixExtensions(pluginsDir) {
|
|
37
86
|
await fixExtensionTrees();
|
|
87
|
+
await fixOpencodeEntry();
|
|
38
88
|
console.log(' Doctor --fix: restoring plugin extension files');
|
|
39
|
-
const
|
|
40
|
-
const pluginExtDir = path.join(tersioPluginDir, 'extensions');
|
|
89
|
+
const pluginExtDir = path.join(pluginsDir, 'node_modules', '@krtclcdy', 'tersio', 'extensions');
|
|
41
90
|
await fs.mkdir(pluginExtDir, { recursive: true });
|
|
42
91
|
// rule.md is skipped: its live content is the upstream fetch, not the bundle.
|
|
43
|
-
const
|
|
44
|
-
for (const file of bundledTree) {
|
|
92
|
+
for (const file of TREE_FILES.filter((f) => f !== 'caveman-session/rule.md')) {
|
|
45
93
|
const text = await readTextIfExists(sourcePath(file));
|
|
46
94
|
if (text === null)
|
|
47
95
|
sayTagged(` [warn] bundled source missing: ${sourcePath(file)}`);
|
|
48
96
|
else
|
|
49
|
-
await writeIfChanged(path.join(pluginExtDir, file), text, { dryRun, verbose });
|
|
97
|
+
await writeIfChanged(path.join(pluginExtDir, ...file.split('/')), text, { dryRun, verbose });
|
|
50
98
|
}
|
|
51
|
-
// Restore only a missing rule; a stale one is /ai-addons' to report.
|
|
52
99
|
const ruleDest = path.join(pluginExtDir, 'caveman-session', 'rule.md');
|
|
53
100
|
if ((await readTextIfExists(ruleDest)) === null) {
|
|
54
101
|
const bundled = await readTextIfExists(path.join(EXT_DIR, 'caveman-session', 'rule.md'));
|
|
@@ -66,6 +113,14 @@ async function fixExtensions(pluginsDir) {
|
|
|
66
113
|
console.log(' [warn] Caveman rule unreachable and no bundled rule exists');
|
|
67
114
|
}
|
|
68
115
|
}
|
|
116
|
+
// Writing is not atomic, so re-read rather than assume the tree is whole.
|
|
117
|
+
if (dryRun)
|
|
118
|
+
return;
|
|
119
|
+
const missing = TREE_FILES.filter((file) => !existsSync(path.join(pluginExtDir, ...file.split('/'))));
|
|
120
|
+
if (missing.length > 0)
|
|
121
|
+
sayTagged(` [fail] tree incomplete: ${missing.join(', ')}`);
|
|
122
|
+
else
|
|
123
|
+
sayTagged(` [ok] extension tree: ${TREE_FILES.length} files`);
|
|
69
124
|
}
|
|
70
125
|
async function fixRegistrations(agentDir, pluginsDir) {
|
|
71
126
|
console.log(' Doctor --fix: repairing config.yml registrations');
|
|
@@ -120,12 +175,16 @@ async function fixRtk(binDir) {
|
|
|
120
175
|
httpsGet(checksAsset.browser_download_url).catch(() => null),
|
|
121
176
|
httpsDownload(asset.browser_download_url, archivePath),
|
|
122
177
|
]);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
178
|
+
// Same rule as the installer: an unverifiable binary is not installed.
|
|
179
|
+
const unverified = (why) => new Error(`${why} for ${asset.name}. Re-run with --allow-unverified to accept it`);
|
|
180
|
+
if (!checks)
|
|
181
|
+
throw unverified('checksums.txt could not be downloaded');
|
|
182
|
+
const expected = parseChecksum(checks, asset.name);
|
|
183
|
+
if (!expected)
|
|
184
|
+
throw unverified('checksums.txt has no entry');
|
|
185
|
+
if (await sha256File(archivePath) !== expected)
|
|
186
|
+
throw new Error(`checksum mismatch for ${asset.name}`);
|
|
187
|
+
debug('RTK checksum verified');
|
|
129
188
|
const extractDir = path.join(tmpDir, 'extracted');
|
|
130
189
|
await fs.mkdir(extractDir, { recursive: true });
|
|
131
190
|
if (asset.name.endsWith('.zip'))
|
|
@@ -147,6 +206,14 @@ async function fixRtk(binDir) {
|
|
|
147
206
|
}
|
|
148
207
|
if (!(await wireRtkOmp(binDest, { dryRun, verbose })))
|
|
149
208
|
throw new Error('rtk wiring failed');
|
|
209
|
+
// Fail-open: a missing host wire must not fail the repair.
|
|
210
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
211
|
+
if (existsSync(path.join(home, '.config', 'opencode', 'plugins', 'tersio'))) {
|
|
212
|
+
await wireRtkOpencode(binDest, { dryRun, verbose });
|
|
213
|
+
}
|
|
214
|
+
if (existsSync(path.join(home, '.pi', 'agent', 'extensions'))) {
|
|
215
|
+
await wireRtkPi(binDest, { dryRun, verbose });
|
|
216
|
+
}
|
|
150
217
|
}
|
|
151
218
|
async function fixPonytail(pluginsDir) {
|
|
152
219
|
console.log(' Doctor --fix: restoring bundled Ponytail');
|