@krtclcdy/tersio 2.21.0 → 2.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -15
- package/cli/common.js +16 -4
- package/cli/currency.js +2 -2
- package/cli/dashboard.js +313 -52
- package/cli/doctor-fix.js +12 -12
- package/cli/doctor.js +21 -52
- package/cli/install.js +31 -60
- package/cli/profile.js +1 -1
- package/cli/settings.js +132 -9
- package/cli/uninstall.js +4 -4
- package/cli/update.js +10 -20
- package/cli/usage.js +1 -1
- package/dashboard/dist/index.html +65 -0
- package/extensions/ai-addons-updater/index.js +13 -30
- package/extensions/ai-addons-updater/index.ts +11 -29
- package/extensions/lib/utils.js +26 -1
- package/extensions/lib/utils.ts +28 -1
- package/extensions/shared/pricing.js +2 -2
- package/extensions/shared/pricing.ts +2 -1
- package/extensions/shared/usage-ledger.js +4 -3
- package/extensions/shared/usage-ledger.ts +4 -3
- package/extensions/shared/usage-store.js +7 -7
- package/extensions/shared/usage-store.ts +7 -6
- package/extensions/tersio-commands/index.js +10 -13
- package/extensions/tersio-commands/index.ts +10 -13
- package/package.json +22 -36
- package/tersio.js +9 -9
- package/dashboard/app.js +0 -1495
- package/dashboard/styles.css +0 -304
- package/dashboard/template.html +0 -410
- /package/dashboard/{brand.webp → dist/brand.webp} +0 -0
package/README.md
CHANGED
|
@@ -18,17 +18,16 @@
|
|
|
18
18
|
curl -fsSL https://github.com/KurutoDenzeru/tersio/releases/latest/download/install.sh | sh
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Prefer OMP-managed updates
|
|
21
|
+
Prefer OMP-managed updates? Install as an OMP plugin instead:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
omp plugin install @krtclcdy/tersio
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
All modes load always. Only the updater stays toggleable:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
omp plugin
|
|
31
|
-
omp plugin features @krtclcdy/tersio --disable rtk
|
|
30
|
+
omp plugin features @krtclcdy/tersio --disable updater
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
Then restart OMP and enable a preset:
|
|
@@ -80,21 +79,26 @@ Measured savings against the same workload without the modes. Token counts are r
|
|
|
80
79
|
|
|
81
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).
|
|
82
81
|
|
|
83
|
-
## 📈
|
|
82
|
+
## 📈 Dashboard
|
|
84
83
|
|
|
85
|
-
`tersio
|
|
84
|
+
`tersio dashboard --open` serves a local dashboard (127.0.0.1 only) that charts your own savings from `~/.tersio/usage.db`. Three views:
|
|
86
85
|
|
|
87
86
|
**Live feed and savings.** Token throughput, cost, savings bento, and recent activity — the top of the dashboard.
|
|
88
87
|
|
|
89
|
-

|
|
90
89
|
|
|
91
90
|
**Top models and recent requests.** Per-model cost breakdown plus the last requests with elapsed time and token speed.
|
|
92
91
|
|
|
93
|
-

|
|
94
93
|
|
|
95
94
|
**Command tools.** Token savings, average rate, and timing per shell tool.
|
|
96
95
|
|
|
97
|
-

|
|
97
|
+
|
|
98
|
+
### Measurement references
|
|
99
|
+
|
|
100
|
+
- **[EcoLogits](https://github.com/mlco2/ecologits)** informs Tersio's local CO₂ and energy estimates. Tersio ports the model to TypeScript; EcoLogits is not a runtime dependency.
|
|
101
|
+
- **[Tokscale](https://github.com/junhoyeo/tokscale)** informs Tersio's Input, Output, Cache Read, and Cache Write accounting from local OMP sessions. Tokscale is a reference, not a runtime dependency.
|
|
98
102
|
|
|
99
103
|
## 🖥️ CLI
|
|
100
104
|
|
|
@@ -105,12 +109,12 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn c
|
|
|
105
109
|
| `tersio reinstall` | Fresh install, preserving the Ponytail package |
|
|
106
110
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health (`--fix` repairs, `--fix=<scope>` one scope, `--dry-run` previews) |
|
|
107
111
|
| `tersio usage` | Ledger-backed usage + savings report |
|
|
108
|
-
| `tersio
|
|
112
|
+
| `tersio dashboard` | Open the Dashboard (`--open`, `--export <file>`, `--port <n>`, `--currency <code>`; serves localhost only) |
|
|
109
113
|
| `tersio reset` | Clear tersio statistics: usage ledger + a watermark that hides pre-reset rows from every derived view (Y/N confirm, `--dry-run`, `--yes`) — session transcripts and RTK history stay intact on disk |
|
|
110
114
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary and its `rtk.ts` OMP wiring) |
|
|
111
115
|
| `tersio version` | Print version |
|
|
112
116
|
|
|
113
|
-
Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`, `--currency <code>` (usage
|
|
117
|
+
Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`, `--currency <code>` (usage/dashboard display currency; flag wins, then the `tersio settings` default, then USD). Legacy `--doctor` / `--uninstall` forms still work.
|
|
114
118
|
|
|
115
119
|
## ⌨️ Commands reference
|
|
116
120
|
|
|
@@ -125,8 +129,8 @@ Mode switches live on their own commands; bare `/tersio` prints status.
|
|
|
125
129
|
| `/tersio status` | Active modes + combo level; state persists, propagates to subagents, and lights the Combo footer indicator. |
|
|
126
130
|
| `/tersio check` | Add-on version check |
|
|
127
131
|
| `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | Update add-ons, preview with dry-run |
|
|
128
|
-
| `/tersio
|
|
129
|
-
| `/tersio usage` | Ledger report for this machine (rows in `~/.
|
|
132
|
+
| `/tersio dashboard` | Open the Dashboard. |
|
|
133
|
+
| `/tersio usage` | Ledger report for this machine (rows in `~/.tersio/usage.db`, local only) |
|
|
130
134
|
| `/tersio help` | This table |
|
|
131
135
|
| `/ai-addons` | Add-on updater alias, still works. |
|
|
132
136
|
|
|
@@ -136,7 +140,7 @@ Mode switches live on their own commands; bare `/tersio` prints status.
|
|
|
136
140
|
|---|---|
|
|
137
141
|
| Caveman / RTK / Updater / Combo extensions | `~/.omp/agent/extensions/{caveman-session,rtk-session,ai-addons-updater,combo-toggle}/` |
|
|
138
142
|
| 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 |
|
|
139
|
-
| Ponytail package | `~/.omp/plugins/node_modules/@dietrichgebert/ponytail/` |
|
|
143
|
+
| Ponytail package (bundled tersio dependency — one Plugins row, updates with `tersio update`) | `~/.omp/plugins/node_modules/@dietrichgebert/ponytail/` |
|
|
140
144
|
| RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows) |
|
|
141
145
|
| Extension registrations | `~/.omp/agent/config.yml` |
|
|
142
146
|
|
|
@@ -150,7 +154,7 @@ User-level installs also register the package in `~/.omp/plugins` (visible in OM
|
|
|
150
154
|
|
|
151
155
|
**RTK missing or not executable:** run `tersio reinstall`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
|
|
152
156
|
|
|
153
|
-
**RTK commands not metered in the
|
|
157
|
+
**RTK commands not metered in the Dashboard:** check `tersio doctor` — the `RTK OMP wiring (rtk.ts)` row must be ok. Missing: run `rtk init -g --agent omp` (needs rtk ≥ 0.49) and restart OMP. Native tool calls (`read`/`edit`/`eval`) stay unmetered — only bash tool calls pass through rtk.
|
|
154
158
|
|
|
155
159
|
**Checksum warning or failure:** the installer aborts on RTK checksum mismatch but warns and continues when checksum metadata is unavailable; `/tersio update rtk` aborts when metadata is missing.
|
|
156
160
|
|
package/cli/common.js
CHANGED
|
@@ -20,6 +20,7 @@ const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('../package.
|
|
|
20
20
|
const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
|
|
21
21
|
const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'review']);
|
|
22
22
|
const RTK_DEFAULTS = new Set(['on', 'off']);
|
|
23
|
+
const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
23
24
|
// ponytail: Combo preset implies all three modes. Mirrors COMBO_LEVELS in
|
|
24
25
|
// extensions/shared/session-state.ts (rtk as boolean here). Single source.
|
|
25
26
|
const COMBO_PRESET_MODES = {
|
|
@@ -47,10 +48,20 @@ function flagValue(name) {
|
|
|
47
48
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
48
49
|
}
|
|
49
50
|
// --- CLI flags ---
|
|
50
|
-
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true,
|
|
51
|
+
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, dashboard: true, reset: true, settings: true, version: true, help: true };
|
|
51
52
|
const args = process.argv.slice(2);
|
|
52
53
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
53
54
|
const command = commandArg?.toLowerCase() || null;
|
|
55
|
+
// Positional after the command: `tersio settings diagnosis` jumps straight
|
|
56
|
+
// to one prompt instead of walking the whole chain. Index-based so flag
|
|
57
|
+
// values (e.g. `--combo-default balanced`) never count as positionals.
|
|
58
|
+
const settingArg = (() => {
|
|
59
|
+
const at = args.indexOf(commandArg ?? '');
|
|
60
|
+
const next = at >= 0 ? args[at + 1] : undefined;
|
|
61
|
+
if (!next || next.startsWith('-'))
|
|
62
|
+
return null;
|
|
63
|
+
return next.toLowerCase();
|
|
64
|
+
})();
|
|
54
65
|
const unknownCommand = command !== null && !COMMANDS[command];
|
|
55
66
|
const install = command === 'install';
|
|
56
67
|
const update = command === 'update';
|
|
@@ -64,13 +75,13 @@ const verbose = args.includes('--verbose');
|
|
|
64
75
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
65
76
|
const fix = args.includes('--fix') || args.some((a) => a.startsWith('--fix='));
|
|
66
77
|
const usage = command === 'usage' || args.includes('--usage');
|
|
67
|
-
const
|
|
78
|
+
const dashboard = command === 'dashboard';
|
|
68
79
|
const reset = command === 'reset';
|
|
69
80
|
const settings = command === 'settings';
|
|
70
81
|
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
71
82
|
const dashboardOpen = args.includes('--open');
|
|
72
83
|
const dashboardExport = flagValue('--export') ?? null;
|
|
73
|
-
// Display currency for usage/
|
|
84
|
+
// Display currency for usage/dashboard: --currency flag wins, then the stored
|
|
74
85
|
// plugin default (tersio settings), then USD.
|
|
75
86
|
const currency = parseCurrencyFlag(flagValue('--currency')) ?? readStoredCurrency();
|
|
76
87
|
const currencyGiven = flagValue('--currency') !== undefined;
|
|
@@ -90,6 +101,7 @@ const comboDefaultFlag = parseEnum(flagValue('--combo-default'), COMBO_DEFAULTS,
|
|
|
90
101
|
const cavemanDefaultFlag = parseEnum(flagValue('--caveman-default'), CAVEMAN_DEFAULTS, '--caveman-default');
|
|
91
102
|
const ponytailDefaultFlag = parseEnum(flagValue('--ponytail-default'), PONYTAIL_DEFAULTS, '--ponytail-default');
|
|
92
103
|
const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rtk-default');
|
|
104
|
+
const diagScheduleFlag = parseEnum(flagValue('--diag-schedule'), DIAG_SCHEDULES, '--diag-schedule');
|
|
93
105
|
const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
|
|
94
106
|
.some((flag) => flag !== undefined);
|
|
95
107
|
function debug(...a) {
|
|
@@ -286,4 +298,4 @@ function relTime(ageMs) {
|
|
|
286
298
|
const weeks = Math.floor(days / 7);
|
|
287
299
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
288
300
|
}
|
|
289
|
-
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, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage,
|
|
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, };
|
package/cli/currency.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/currency.ts — display-currency helpers (USD-base, offline snapshot rates).
|
|
2
|
-
// Same 10 currencies, symbols, and snapshot rates as the
|
|
3
|
-
// converter (dashboard/app
|
|
2
|
+
// Same 10 currencies, symbols, and snapshot rates as the dashboard's
|
|
3
|
+
// converter (dashboard/app); the dashboard replaces rates with live
|
|
4
4
|
// frankfurter figures when reachable, the CLI always uses the snapshot.
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import os from 'node:os';
|
package/cli/dashboard.js
CHANGED
|
@@ -1,35 +1,269 @@
|
|
|
1
|
-
// cli/dashboard.ts —
|
|
2
|
-
// template (no deps, no build) plus a /data.json endpoint from the ledger.
|
|
1
|
+
// cli/dashboard.ts — serves the built shadcn Dashboard with local usage APIs.
|
|
3
2
|
// Binds 127.0.0.1 only; --export writes a file://-ready file instead.
|
|
4
3
|
// Note: no Promise.withResolvers here — engines still allow Node 20.12,
|
|
5
4
|
// which lacks it; the listening server itself keeps the process alive.
|
|
6
5
|
import { spawn } from 'node:child_process';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
7
8
|
import { promises as fs } from 'node:fs';
|
|
8
9
|
import http from 'node:http';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
13
|
+
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
12
14
|
import { summarizeUsage } from './usage.js';
|
|
13
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';
|
|
14
17
|
import { storedProfile, writePluginSettings } from './profile.js';
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const DASHBOARD_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard', 'dist');
|
|
19
|
+
const DASHBOARD_INDEX = path.join(DASHBOARD_DIST, 'index.html');
|
|
20
|
+
const DASHBOARD_BRAND = path.join(DASHBOARD_DIST, 'brand.webp');
|
|
21
|
+
function requireDashboardBundle() {
|
|
22
|
+
if (existsSync(DASHBOARD_INDEX))
|
|
23
|
+
return;
|
|
24
|
+
throw new Error('Dashboard bundle missing. Run `bun run build` before using `tersio dashboard`.');
|
|
25
|
+
}
|
|
26
|
+
function contentType(file) {
|
|
27
|
+
if (file.endsWith('.html'))
|
|
28
|
+
return 'text/html; charset=utf-8';
|
|
29
|
+
if (file.endsWith('.js'))
|
|
30
|
+
return 'text/javascript; charset=utf-8';
|
|
31
|
+
if (file.endsWith('.css'))
|
|
32
|
+
return 'text/css; charset=utf-8';
|
|
33
|
+
if (file.endsWith('.webp'))
|
|
34
|
+
return 'image/webp';
|
|
35
|
+
if (file.endsWith('.svg'))
|
|
36
|
+
return 'image/svg+xml';
|
|
37
|
+
if (file.endsWith('.png'))
|
|
38
|
+
return 'image/png';
|
|
39
|
+
if (file.endsWith('.woff2'))
|
|
40
|
+
return 'font/woff2';
|
|
41
|
+
return 'application/octet-stream';
|
|
42
|
+
}
|
|
21
43
|
async function readSegment(file) {
|
|
22
44
|
return fs.readFile(file, 'utf8');
|
|
23
45
|
}
|
|
24
|
-
async function
|
|
25
|
-
const
|
|
26
|
-
return `data:image/webp;base64,${
|
|
46
|
+
async function brandDataUri() {
|
|
47
|
+
const brand = await fs.readFile(DASHBOARD_BRAND);
|
|
48
|
+
return `data:image/webp;base64,${brand.toString('base64')}`;
|
|
27
49
|
}
|
|
28
50
|
function dataJson() {
|
|
29
51
|
return JSON.stringify(summarizeUsage(readUsage()));
|
|
30
52
|
}
|
|
53
|
+
// Local-only health + diagnosis for the settings modal. No network: version
|
|
54
|
+
// probes run with short timeouts, file checks are existsSync.
|
|
55
|
+
function ompPath() {
|
|
56
|
+
const names = process.platform === 'win32' ? ['omp.cmd', 'omp.exe', 'omp.bat', 'omp'] : ['omp'];
|
|
57
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
const candidate = path.join(dir, name);
|
|
60
|
+
try {
|
|
61
|
+
if (statSync(candidate).isFile())
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
catch { /* continue searching PATH */ }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function ompVersion(bin = ompPath()) {
|
|
70
|
+
if (!bin)
|
|
71
|
+
return null;
|
|
72
|
+
try {
|
|
73
|
+
if (process.platform === 'win32') {
|
|
74
|
+
const exe = process.env.ComSpec || 'cmd.exe';
|
|
75
|
+
return execFileSync(exe, ['/d', '/s', '/c', bin, '--version'], { encoding: 'utf8', timeout: 5000, windowsHide: true }).trim() || null;
|
|
76
|
+
}
|
|
77
|
+
return execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 5000, windowsHide: true }).trim() || null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function rtkVersion(bin) {
|
|
84
|
+
try {
|
|
85
|
+
return execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 5000, windowsHide: true }).trim().split(/\r?\n/)[0] || null;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function ompDefaultModel() {
|
|
92
|
+
try {
|
|
93
|
+
const text = readFileSync(path.join(OMP_AGENT_DIR, 'config.yml'), 'utf8');
|
|
94
|
+
const m = text.match(/^\s*default\s*:\s*(.+?)\s*$/m);
|
|
95
|
+
return m ? m[1].replace(/^['"]|['"]$/g, '') : null;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function healthJson() {
|
|
102
|
+
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
103
|
+
const rtkPresent = existsSync(rtkBin);
|
|
104
|
+
const detectedOmpPath = ompPath();
|
|
105
|
+
return JSON.stringify({
|
|
106
|
+
tersio: PACKAGE_VERSION,
|
|
107
|
+
node: process.version,
|
|
108
|
+
platform: `${process.platform}/${process.arch}`,
|
|
109
|
+
omp: ompVersion(detectedOmpPath),
|
|
110
|
+
ompPath: detectedOmpPath,
|
|
111
|
+
provider: ompDefaultModel(),
|
|
112
|
+
rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin },
|
|
113
|
+
home: tersioHomePath(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function tersioHomePath() {
|
|
117
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.tersio');
|
|
118
|
+
}
|
|
119
|
+
const DIAG_TTL_MS = {
|
|
120
|
+
manual: 0,
|
|
121
|
+
daily: 24 * 3600 * 1000,
|
|
122
|
+
weekly: 7 * 24 * 3600 * 1000,
|
|
123
|
+
monthly: 30 * 24 * 3600 * 1000,
|
|
124
|
+
};
|
|
125
|
+
function diagPaths() {
|
|
126
|
+
// Hermetic env (tests) overrides the DB path: keep the report next to it
|
|
127
|
+
// so test runs never touch the real ~/.tersio/diag.json.
|
|
128
|
+
const dbOverride = process.env.TERSIO_USAGE_DB;
|
|
129
|
+
if (dbOverride)
|
|
130
|
+
return { report: path.join(path.dirname(dbOverride), 'diag.json') };
|
|
131
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
132
|
+
return { report: path.join(home, '.tersio', 'diag.json') };
|
|
133
|
+
}
|
|
134
|
+
function readDiagReport() {
|
|
135
|
+
try {
|
|
136
|
+
const raw = JSON.parse(readFileSync(diagPaths().report, 'utf8'));
|
|
137
|
+
if (!Array.isArray(raw.rows) || typeof raw.checkedAt !== 'number')
|
|
138
|
+
return null;
|
|
139
|
+
const schedule = raw.schedule === 'daily' || raw.schedule === 'weekly' || raw.schedule === 'monthly' ? raw.schedule : 'manual';
|
|
140
|
+
return { rows: raw.rows, checkedAt: raw.checkedAt, schedule };
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function writeDiagReport(report) {
|
|
147
|
+
try {
|
|
148
|
+
mkdirSync(path.dirname(diagPaths().report), { recursive: true });
|
|
149
|
+
writeFileSync(diagPaths().report, JSON.stringify(report) + '\n', 'utf8');
|
|
150
|
+
}
|
|
151
|
+
catch { /* best-effort */ }
|
|
152
|
+
}
|
|
153
|
+
function ageStr(p) {
|
|
154
|
+
try {
|
|
155
|
+
return relAge(Date.now() - statSync(p).mtimeMs);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function absTime(p) {
|
|
162
|
+
try {
|
|
163
|
+
const d = new Date(statSync(p).mtimeMs);
|
|
164
|
+
const q = (n) => String(n).padStart(2, '0');
|
|
165
|
+
const h24 = d.getHours();
|
|
166
|
+
return `${q(d.getMonth() + 1)}-${q(d.getDate())}-${d.getFullYear()}, ${q(h24 % 12 || 12)}:${q(d.getMinutes())} ${h24 >= 12 ? 'PM' : 'AM'}`;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function relAge(ms) {
|
|
173
|
+
const mins = Math.floor(ms / 60000);
|
|
174
|
+
if (mins < 1)
|
|
175
|
+
return 'just now';
|
|
176
|
+
if (mins < 60)
|
|
177
|
+
return `${mins} min${mins === 1 ? '' : 's'} ago`;
|
|
178
|
+
const hours = Math.floor(mins / 60);
|
|
179
|
+
if (hours < 24)
|
|
180
|
+
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
181
|
+
const days = Math.floor(hours / 24);
|
|
182
|
+
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
183
|
+
}
|
|
184
|
+
function computeDoctorRows() {
|
|
185
|
+
const rows = [];
|
|
186
|
+
const extDir = path.join(OMP_AGENT_DIR, 'extensions');
|
|
187
|
+
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
188
|
+
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
189
|
+
const ok = (p) => existsSync(p);
|
|
190
|
+
const ext = (label, p) => {
|
|
191
|
+
rows.push({ label, ok: ok(p), detail: ok(p) ? 'installed' : p, group: 'Extensions' });
|
|
192
|
+
};
|
|
193
|
+
const addon = (label, present, detail, group = 'Add-ons') => {
|
|
194
|
+
rows.push({ label, ok: present, detail, group });
|
|
195
|
+
};
|
|
196
|
+
ext('Caveman extension', path.join(extDir, 'caveman-session', 'index.ts'));
|
|
197
|
+
ext('RTK extension', path.join(extDir, 'rtk-session', 'index.ts'));
|
|
198
|
+
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');
|
|
200
|
+
const ruleAge = ageStr(rule);
|
|
201
|
+
const ruleAt = absTime(rule);
|
|
202
|
+
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);
|
|
207
|
+
const wiring = path.join(extDir, 'rtk.ts');
|
|
208
|
+
addon('RTK OMP wiring (rtk.ts)', ok(wiring), ok(wiring) ? 'loaded' : wiring);
|
|
209
|
+
let ponytailVer = null;
|
|
210
|
+
try {
|
|
211
|
+
ponytailVer = JSON.parse(readFileSync(ponytailPkg, 'utf8')).version ?? null;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
ponytailVer = null;
|
|
215
|
+
}
|
|
216
|
+
const ponytailAge = ageStr(ponytailPkg);
|
|
217
|
+
const ponytailAt = absTime(ponytailPkg);
|
|
218
|
+
addon('Ponytail', ponytailVer !== null, ponytailVer && ponytailAge && ponytailAt ? `${ponytailVer} (updated ${ponytailAge} · ${ponytailAt})` : ponytailPkg);
|
|
219
|
+
const prices = pricesCachePath();
|
|
220
|
+
const pricesAge = ageStr(prices);
|
|
221
|
+
const pricesAt = absTime(prices);
|
|
222
|
+
addon('Prices feed', pricesAge !== null, pricesAge && pricesAt ? `(pulled ${pricesAge} · ${pricesAt})` : prices, 'Usage');
|
|
223
|
+
return rows;
|
|
224
|
+
}
|
|
225
|
+
// Retained diagnosis: recompute when forced, missing, or older than the
|
|
226
|
+
// schedule. Otherwise return the saved report so users never re-run.
|
|
227
|
+
// Labels retired from the report. A saved report containing any of them
|
|
228
|
+
// is stale by definition and always recomputes.
|
|
229
|
+
const RETIRED_DIAG_LABELS = new Set([
|
|
230
|
+
'OMP CLI',
|
|
231
|
+
'Tersio CLI',
|
|
232
|
+
'Tersio commands',
|
|
233
|
+
'Mode reinforcement',
|
|
234
|
+
'Combo registered',
|
|
235
|
+
'Combo extension',
|
|
236
|
+
'Self plugin',
|
|
237
|
+
'Ponytail registered',
|
|
238
|
+
'Usage store',
|
|
239
|
+
]);
|
|
240
|
+
function isRetiredReport(report) {
|
|
241
|
+
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
242
|
+
}
|
|
243
|
+
// Retained diagnosis: recompute when forced, missing, retired, or older
|
|
244
|
+
// than the schedule. Otherwise return the saved report.
|
|
245
|
+
function getDoctorReport(force) {
|
|
246
|
+
const saved = readDiagReport();
|
|
247
|
+
const ttl = saved ? DIAG_TTL_MS[saved.schedule] : 0;
|
|
248
|
+
// Manual always recomputes on open; dated schedules reuse the saved
|
|
249
|
+
// report until stale. Otherwise a retained snapshot hides new rows.
|
|
250
|
+
if (!force && saved && !isRetiredReport(saved) && ttl > 0 && Date.now() - saved.checkedAt < ttl)
|
|
251
|
+
return saved;
|
|
252
|
+
const report = { rows: computeDoctorRows(), checkedAt: Date.now(), schedule: saved?.schedule ?? 'manual' };
|
|
253
|
+
writeDiagReport(report);
|
|
254
|
+
return report;
|
|
255
|
+
}
|
|
256
|
+
function setDiagSchedule(schedule) {
|
|
257
|
+
const base = readDiagReport() ?? { rows: computeDoctorRows(), checkedAt: Date.now(), schedule };
|
|
258
|
+
base.schedule = schedule;
|
|
259
|
+
writeDiagReport(base);
|
|
260
|
+
return base;
|
|
261
|
+
}
|
|
262
|
+
function readDiagSchedule() {
|
|
263
|
+
return readDiagReport()?.schedule ?? 'manual';
|
|
264
|
+
}
|
|
31
265
|
// Persist the dashboard's picker choice so close → reopen keeps it: each
|
|
32
|
-
// `tersio
|
|
266
|
+
// `tersio dashboard` run serves a fresh ephemeral port (a new origin), so the
|
|
33
267
|
// browser's localStorage alone cannot survive a restart. The stored default
|
|
34
268
|
// feeds data.json and `tersio usage` on the next run.
|
|
35
269
|
async function saveDashboardCurrency(raw) {
|
|
@@ -55,33 +289,33 @@ function openBrowser(url) {
|
|
|
55
289
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
56
290
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
57
291
|
}
|
|
292
|
+
// Replacer functions throughout: session data routinely contains `$'`
|
|
293
|
+
// sequences (shell quoting in tool details), which String.replace would
|
|
294
|
+
// expand as match-suffix patterns and corrupt the file.
|
|
295
|
+
// RTK-metered command lines routinely contain literal `</script>` (Vue
|
|
296
|
+
// SFC probes), which would close the inlined <script> early and dump the
|
|
297
|
+
// rest of the JSON as page text. Escape it; JSON.parse never sees the
|
|
298
|
+
// backslash form inside a string literal, the browser decodes it first.
|
|
299
|
+
function escapeInline(json) {
|
|
300
|
+
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
301
|
+
}
|
|
302
|
+
async function exportDashboard(exportFile) {
|
|
303
|
+
requireDashboardBundle();
|
|
304
|
+
const [bundle, icon] = await Promise.all([readSegment(DASHBOARD_INDEX), brandDataUri()]);
|
|
305
|
+
const inline = bundle
|
|
306
|
+
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({ data: JSON.parse(dataJson()), health: JSON.parse(healthJson()), doctor: getDoctorReport(false) }))};`)
|
|
307
|
+
.replace(/href="brand\.webp"/g, () => `href="${icon}"`)
|
|
308
|
+
.replace(/src="brand\.webp"/g, () => `src="${icon}"`)
|
|
309
|
+
.replace('fetch("brand.webp")', () => `Promise.resolve({ ok: true, blob: async () => new Blob([atob("${icon.split(',')[1]}")], { type: "image/webp" }) })`);
|
|
310
|
+
await fs.writeFile(exportFile, inline, 'utf8');
|
|
311
|
+
console.log(`[ok] Dashboard exported → ${exportFile}`);
|
|
312
|
+
}
|
|
58
313
|
async function runDashboard(options) {
|
|
59
314
|
if (options.exportFile) {
|
|
60
|
-
|
|
61
|
-
readSegment(TEMPLATE), readSegment(STYLES), readSegment(APP), faviconDataUri(),
|
|
62
|
-
]);
|
|
63
|
-
// Replacer functions throughout: session data routinely contains `$'`
|
|
64
|
-
// sequences (shell quoting in tool details), which String.replace would
|
|
65
|
-
// expand as match-suffix patterns and corrupt the file.
|
|
66
|
-
// RTK-metered command lines routinely contain literal `</script>` (Vue
|
|
67
|
-
// SFC probes), which would close the inlined <script> early and dump the
|
|
68
|
-
// rest of the JSON as page text. Escape it; JSON.parse never sees the
|
|
69
|
-
// backslash form inside a string literal, the browser decodes it first.
|
|
70
|
-
const inline = template
|
|
71
|
-
.replace('<link rel="stylesheet" href="styles.css">', () => `<style>\n${css}</style>`)
|
|
72
|
-
.replace('<script src="app.js" defer></script>', () => `<script>\n${js}</script>`)
|
|
73
|
-
.replace("fetch('data.json')", () => `Promise.resolve({ json: function () { return ${dataJson().replace(/<\/(script)/gi, '<\\/$1')}; } })`)
|
|
74
|
-
// Exported file runs on file:// where load() early-returns before the
|
|
75
|
-
// stub above ever runs, so nothing ever renders. Drop that guard in
|
|
76
|
-
// the export only; template.html keeps it to avoid 404 polling loops.
|
|
77
|
-
.replace("if (window.location.protocol === 'file:') return;", () => `if (false) return;`)
|
|
78
|
-
.replace('href="brand.webp"', () => `href="${icon}"`)
|
|
79
|
-
.replace('src="brand.webp"', () => `src="${icon}"`);
|
|
80
|
-
await fs.writeFile(options.exportFile, inline, 'utf8');
|
|
81
|
-
console.log(`[ok] gain exported → ${options.exportFile}`);
|
|
315
|
+
await exportDashboard(options.exportFile);
|
|
82
316
|
return;
|
|
83
317
|
}
|
|
84
|
-
|
|
318
|
+
requireDashboardBundle();
|
|
85
319
|
const server = http.createServer(async (req, res) => {
|
|
86
320
|
if (req.url === '/reset' && req.method === 'POST') {
|
|
87
321
|
const rows = clearUsageLedger();
|
|
@@ -114,42 +348,69 @@ async function runDashboard(options) {
|
|
|
114
348
|
res.end(dataJson());
|
|
115
349
|
return;
|
|
116
350
|
}
|
|
117
|
-
if (req.url === '/
|
|
118
|
-
res.writeHead(200, { 'Content-Type': '
|
|
119
|
-
res.end(
|
|
351
|
+
if (req.url === '/health') {
|
|
352
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
353
|
+
res.end(healthJson());
|
|
120
354
|
return;
|
|
121
355
|
}
|
|
122
|
-
if (req.url === '/
|
|
123
|
-
|
|
124
|
-
|
|
356
|
+
if (req.url === '/doctor' && req.method === 'POST') {
|
|
357
|
+
let schedule = 'manual';
|
|
358
|
+
try {
|
|
359
|
+
const raw = JSON.parse(await readBody(req)).schedule;
|
|
360
|
+
if (raw === 'daily' || raw === 'weekly' || raw === 'monthly' || raw === 'manual')
|
|
361
|
+
schedule = raw;
|
|
362
|
+
}
|
|
363
|
+
catch { /* keep manual */ }
|
|
364
|
+
const report = schedule === 'manual' ? getDoctorReport(true) : setDiagSchedule(schedule);
|
|
365
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
366
|
+
res.end(JSON.stringify(report));
|
|
125
367
|
return;
|
|
126
368
|
}
|
|
127
|
-
if (req.url === '/
|
|
369
|
+
if (req.url === '/doctor/fix' && req.method === 'POST') {
|
|
128
370
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
371
|
+
const { runDoctorRepairs } = await import('./doctor-fix.js');
|
|
372
|
+
const failed = await runDoctorRepairs(['extensions', 'registrations', 'ponytail']);
|
|
373
|
+
getDoctorReport(true);
|
|
374
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
375
|
+
res.end(JSON.stringify({ ok: failed.length === 0, failed }));
|
|
132
376
|
}
|
|
133
|
-
catch {
|
|
134
|
-
res.writeHead(
|
|
135
|
-
res.end();
|
|
377
|
+
catch (e) {
|
|
378
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
379
|
+
res.end(JSON.stringify({ ok: false, failed: ['fix'], error: e.message }));
|
|
136
380
|
}
|
|
137
381
|
return;
|
|
138
382
|
}
|
|
383
|
+
if (req.url === '/doctor' || req.url?.startsWith('/doctor?')) {
|
|
384
|
+
const fresh = (req.url.split('?')[1] ?? '').includes('fresh=1');
|
|
385
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
386
|
+
res.end(JSON.stringify(getDoctorReport(fresh)));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const name = (req.url ?? '/').split('?')[0];
|
|
390
|
+
const file = path.normalize(path.join(DASHBOARD_DIST, name === '/' ? 'index.html' : name.slice(1)));
|
|
391
|
+
if (file === DASHBOARD_DIST || file.startsWith(DASHBOARD_DIST + path.sep)) {
|
|
392
|
+
try {
|
|
393
|
+
const body = await fs.readFile(file);
|
|
394
|
+
res.writeHead(200, { 'Content-Type': contentType(file) });
|
|
395
|
+
res.end(body);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
catch { /* fall through to index */ }
|
|
399
|
+
}
|
|
139
400
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
140
|
-
res.end(
|
|
401
|
+
res.end(await readSegment(DASHBOARD_INDEX));
|
|
141
402
|
});
|
|
142
403
|
server.listen(options.port, '127.0.0.1', () => {
|
|
143
404
|
try {
|
|
144
|
-
process.title = 'tersio
|
|
405
|
+
process.title = 'tersio dashboard';
|
|
145
406
|
}
|
|
146
407
|
catch { /* non-POSIX shells keep node */ }
|
|
147
408
|
const address = server.address();
|
|
148
409
|
const port = typeof address === 'object' && address ? address.port : options.port;
|
|
149
410
|
const url = `http://127.0.0.1:${port}`;
|
|
150
|
-
console.log(`[ok]
|
|
411
|
+
console.log(`[ok] Dashboard live → ${url} (Ctrl-C to stop)`);
|
|
151
412
|
if (options.open)
|
|
152
413
|
openBrowser(url);
|
|
153
414
|
});
|
|
154
415
|
}
|
|
155
|
-
export { runDashboard, saveDashboardCurrency };
|
|
416
|
+
export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule };
|
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,
|
|
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';
|
|
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';
|
|
@@ -23,8 +23,6 @@ const SOURCES = [
|
|
|
23
23
|
[path.join(EXT_DIR, 'tersio-commands', 'index.ts'), path.join('tersio-commands', 'index.ts')],
|
|
24
24
|
[path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts'), path.join('shared', 'mode-reinforcement.ts')],
|
|
25
25
|
];
|
|
26
|
-
const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
|
|
27
|
-
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
28
26
|
async function fixExtensions(extDir) {
|
|
29
27
|
console.log(' Doctor --fix: restoring extension files');
|
|
30
28
|
await fs.mkdir(extDir, { recursive: true });
|
|
@@ -130,22 +128,24 @@ async function fixRtk(binDir) {
|
|
|
130
128
|
throw new Error('rtk wiring failed');
|
|
131
129
|
}
|
|
132
130
|
async function fixPonytail(pluginsDir, agentDir) {
|
|
133
|
-
console.log(' Doctor --fix:
|
|
131
|
+
console.log(' Doctor --fix: restoring bundled Ponytail');
|
|
134
132
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
135
133
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
136
134
|
const pkg = await readPluginsPackage(pkgPath);
|
|
137
|
-
|
|
135
|
+
// Drop the legacy separate entry: ponytail is a tersio dependency now.
|
|
136
|
+
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
137
|
+
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
138
|
+
console.log(' [migrate] dropped separate @dietrichgebert/ponytail dependency (now bundled with tersio)');
|
|
139
|
+
}
|
|
140
|
+
if (!(PACKAGE_NAME in pkg.dependencies))
|
|
141
|
+
pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
|
|
138
142
|
if (dryRun) {
|
|
139
|
-
console.log(
|
|
140
|
-
console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
|
|
143
|
+
console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
|
|
141
144
|
return;
|
|
142
145
|
}
|
|
143
146
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
144
|
-
await execNetwork('
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
-
await execNetwork('Refreshing Ponytail package', 'npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 }).catch((e) => {
|
|
148
|
-
throw new Error(`ponytail refresh failed: ${e.message}`);
|
|
147
|
+
await execNetwork('Restoring bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
|
|
148
|
+
throw new Error(`bundled ponytail restore failed: ${e.message}`);
|
|
149
149
|
});
|
|
150
150
|
await ensureExtensionInConfig(path.join(agentDir, 'config.yml'), path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'), 'ponytail', { dryRun, verbose });
|
|
151
151
|
}
|