@krtclcdy/tersio 2.22.1 → 2.24.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 +62 -46
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +55 -55
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +76 -49
- package/cli/doctor-fix.js +61 -44
- package/cli/doctor.js +66 -44
- package/cli/hosts.js +118 -0
- package/cli/install.js +187 -143
- package/cli/interactive.js +27 -1
- package/cli/layer.js +24 -0
- package/cli/manifest.js +36 -0
- package/cli/omp-layer.js +51 -0
- package/cli/pi-layer.js +37 -0
- package/cli/pi-wiring.js +92 -0
- package/cli/profile.js +45 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/settings.js +1 -1
- package/cli/uninstall.js +135 -44
- package/cli/update.js +24 -30
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +36 -25
- package/extensions/ai-addons-updater/index.ts +36 -24
- package/extensions/caveman-session/index.js +23 -18
- package/extensions/caveman-session/index.ts +23 -18
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +63 -41
- package/extensions/combo-toggle/index.ts +61 -48
- package/extensions/lib/utils.js +56 -13
- package/extensions/lib/utils.ts +55 -13
- package/extensions/rtk-session/index.js +20 -19
- package/extensions/rtk-session/index.ts +18 -24
- package/extensions/shared/carbon.js +5 -8
- package/extensions/shared/carbon.ts +5 -8
- package/extensions/shared/host.js +68 -0
- package/extensions/shared/host.ts +104 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +63 -16
- package/extensions/shared/plugin-settings.ts +67 -17
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +13 -24
- package/extensions/shared/rtk-gain.ts +14 -25
- package/extensions/shared/session-state.js +26 -28
- package/extensions/shared/session-state.ts +26 -29
- package/extensions/shared/types.ts +15 -3
- package/extensions/shared/usage-ledger.js +136 -38
- package/extensions/shared/usage-ledger.ts +149 -52
- package/extensions/shared/usage-store.js +14 -12
- package/extensions/shared/usage-store.ts +16 -14
- package/extensions/tersio-commands/index.js +2 -1
- package/extensions/tersio-commands/index.ts +2 -1
- package/package.json +15 -7
- package/tersio.js +6 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/mode-reinforcement.ts +0 -29
package/cli/common.js
CHANGED
|
@@ -16,13 +16,12 @@ 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
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts (rtk as a
|
|
24
|
+
// boolean here), so the combo preset implies all three modes.
|
|
26
25
|
const COMBO_PRESET_MODES = {
|
|
27
26
|
off: { caveman: 'off', rtk: false, ponytail: 'off' },
|
|
28
27
|
medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
|
|
@@ -48,13 +47,12 @@ function flagValue(name) {
|
|
|
48
47
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
49
48
|
}
|
|
50
49
|
// --- CLI flags ---
|
|
51
|
-
const COMMANDS = { install: true, update: true,
|
|
50
|
+
const COMMANDS = { install: true, update: true, doctor: true, uninstall: true, usage: true, dashboard: true, reset: true, settings: true, version: true, help: true };
|
|
52
51
|
const args = process.argv.slice(2);
|
|
53
52
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
54
53
|
const command = commandArg?.toLowerCase() || null;
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// values (e.g. `--combo-default balanced`) never count as positionals.
|
|
54
|
+
// `tersio settings diagnosis` jumps to one prompt instead of walking the
|
|
55
|
+
// chain. Index-based, so flag values never count as positionals.
|
|
58
56
|
const settingArg = (() => {
|
|
59
57
|
const at = args.indexOf(commandArg ?? '');
|
|
60
58
|
const next = at >= 0 ? args[at + 1] : undefined;
|
|
@@ -65,12 +63,11 @@ const settingArg = (() => {
|
|
|
65
63
|
const unknownCommand = command !== null && !COMMANDS[command];
|
|
66
64
|
const install = command === 'install';
|
|
67
65
|
const update = command === 'update';
|
|
68
|
-
const reinstall = command === 'reinstall';
|
|
69
66
|
const showVersion = command === 'version' || args.includes('--version') || args.includes('-v');
|
|
70
67
|
const showHelp = command === 'help' || args.includes('--help') || args.includes('-h');
|
|
71
68
|
const applyUpdate = args.includes('--apply-update');
|
|
72
69
|
const dryRun = args.includes('--dry-run');
|
|
73
|
-
const yes = args.includes('--yes') || args.includes('-y') || install || update ||
|
|
70
|
+
const yes = args.includes('--yes') || args.includes('-y') || install || update || applyUpdate;
|
|
74
71
|
const verbose = args.includes('--verbose');
|
|
75
72
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
76
73
|
const fix = args.includes('--fix') || args.some((a) => a.startsWith('--fix='));
|
|
@@ -81,16 +78,15 @@ const settings = command === 'settings';
|
|
|
81
78
|
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
82
79
|
const dashboardOpen = args.includes('--open');
|
|
83
80
|
const dashboardExport = flagValue('--export') ?? null;
|
|
84
|
-
// Display currency
|
|
85
|
-
// plugin default (tersio settings), then USD.
|
|
81
|
+
// Display currency: --currency flag, then the stored default, then USD.
|
|
86
82
|
const currency = parseCurrencyFlag(flagValue('--currency')) ?? readStoredCurrency();
|
|
87
83
|
const currencyGiven = flagValue('--currency') !== undefined;
|
|
88
84
|
const uninstall = command === 'uninstall' || args.includes('--uninstall');
|
|
89
85
|
const removePonytail = args.includes('--remove-ponytail');
|
|
90
86
|
const keepPonytail = args.includes('--keep-ponytail');
|
|
91
87
|
const removeRtk = args.includes('--remove-rtk');
|
|
92
|
-
//
|
|
93
|
-
//
|
|
88
|
+
// User scope only (all OMP sessions). A bare `--scope user` still parses for
|
|
89
|
+
// old scripts; anything else fails.
|
|
94
90
|
const legacyScope = flagValue('--scope')?.toLowerCase() ?? null;
|
|
95
91
|
if (legacyScope !== null && legacyScope !== 'user') {
|
|
96
92
|
console.error(`[fail] Invalid --scope: ${legacyScope}. Project scope was removed; tersio installs user-level only.`);
|
|
@@ -137,8 +133,8 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
137
133
|
console.log(` [write] ${dest}`);
|
|
138
134
|
return true;
|
|
139
135
|
}
|
|
140
|
-
// omp ships 'extensions: null'
|
|
141
|
-
//
|
|
136
|
+
// omp ships 'extensions: null', and list items under a null scalar break YAML:
|
|
137
|
+
// normalize null/~/[]/empty to a mapping key first.
|
|
142
138
|
const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
|
|
143
139
|
const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
|
|
144
140
|
function normalizeExtensionsKey(lines) {
|
|
@@ -160,14 +156,32 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
160
156
|
const line = ` - ${normalizedPath}`;
|
|
161
157
|
const raw = await readTextIfExists(configPath);
|
|
162
158
|
let lines = (raw || '').split('\n');
|
|
163
|
-
// Drop legacy compiled twins (same dir/name, .js):
|
|
164
|
-
//
|
|
165
|
-
// trailing path so absolute and relative entries are both caught.
|
|
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.
|
|
166
161
|
if (normalizedPath.endsWith('.ts')) {
|
|
167
162
|
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
168
163
|
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
169
164
|
}
|
|
170
|
-
|
|
165
|
+
const matchingEntries = lines.filter((entry) => entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '') === normalizedPath);
|
|
166
|
+
if (matchingEntries.length > 1) {
|
|
167
|
+
if (options.dryRun) {
|
|
168
|
+
if (verbose && !options.quiet)
|
|
169
|
+
console.log(` [dry-run] would remove ${matchingEntries.length - 1} duplicate ${label} registration${matchingEntries.length === 2 ? '' : 's'}`);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
let kept = false;
|
|
173
|
+
lines = lines.filter((entry) => {
|
|
174
|
+
if (entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '') !== normalizedPath)
|
|
175
|
+
return true;
|
|
176
|
+
if (kept)
|
|
177
|
+
return false;
|
|
178
|
+
kept = true;
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
await writeConfigLines(configPath, lines, ` [write] Removed duplicate ${label} registration`, options);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (matchingEntries.length === 1) {
|
|
171
185
|
debug(`${label} already in config.yml`);
|
|
172
186
|
return false;
|
|
173
187
|
}
|
|
@@ -191,43 +205,29 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
191
205
|
await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`, options);
|
|
192
206
|
return true;
|
|
193
207
|
}
|
|
194
|
-
async function
|
|
195
|
-
const normalizedPath = extensionPath.replace(/\\/g, '/');
|
|
196
|
-
const
|
|
197
|
-
|
|
208
|
+
async function removeExtensionFromConfig(configPath, extensionPath, label, options = {}) {
|
|
209
|
+
const normalizedPath = extensionPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
210
|
+
const extensionSuffix = normalizedPath.includes('/extensions/')
|
|
211
|
+
? normalizedPath.slice(normalizedPath.indexOf('/extensions/') + 1)
|
|
212
|
+
: normalizedPath;
|
|
198
213
|
const raw = await readTextIfExists(configPath);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
214
|
+
if (raw === null)
|
|
215
|
+
return false;
|
|
216
|
+
const lines = raw.split('\n');
|
|
217
|
+
const matches = (entry) => {
|
|
218
|
+
const value = entry.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
219
|
+
return value === extensionSuffix || value.endsWith(`/${extensionSuffix}`);
|
|
220
|
+
};
|
|
221
|
+
const kept = lines.filter((entry) => !matches(entry));
|
|
222
|
+
const removed = lines.length - kept.length;
|
|
223
|
+
if (removed === 0)
|
|
208
224
|
return false;
|
|
209
225
|
if (options.dryRun) {
|
|
210
226
|
if (verbose && !options.quiet)
|
|
211
|
-
console.log(` [dry-run] would
|
|
227
|
+
console.log(` [dry-run] would remove ${label} registration${removed === 1 ? '' : 's'}`);
|
|
212
228
|
return true;
|
|
213
229
|
}
|
|
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);
|
|
230
|
+
await writeConfigLines(configPath, kept, ` [write] Removed duplicate ${label} registration`, options);
|
|
231
231
|
return true;
|
|
232
232
|
}
|
|
233
233
|
function readPonytailConfig() {
|
|
@@ -277,8 +277,8 @@ async function ensurePonytailConfigValue(key, value, options = {}) {
|
|
|
277
277
|
return true;
|
|
278
278
|
}, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
|
|
279
279
|
}
|
|
280
|
-
// Read plugins/package.json tolerantly; corrupt or missing
|
|
281
|
-
// with installer-managed defaults.
|
|
280
|
+
// Read plugins/package.json tolerantly; a corrupt or missing file starts
|
|
281
|
+
// fresh with installer-managed defaults.
|
|
282
282
|
async function readPluginsPackage(pkgPath) {
|
|
283
283
|
const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
|
|
284
284
|
return { ...pkg, name: pkg.name || 'omp-plugins', private: true, dependencies: pkg.dependencies || {} };
|
|
@@ -298,4 +298,4 @@ function relTime(ageMs) {
|
|
|
298
298
|
const weeks = Math.floor(days / 7);
|
|
299
299
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
300
300
|
}
|
|
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,
|
|
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, };
|
package/cli/currency.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
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
|
-
import
|
|
7
|
-
import path from 'node:path';
|
|
6
|
+
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
8
7
|
const CURRENCY_CODES = [
|
|
9
8
|
'USD',
|
|
10
9
|
'PHP',
|
|
@@ -36,18 +35,14 @@ function parseCurrencyFlag(raw) {
|
|
|
36
35
|
// USD when missing, corrupt, or holding an unknown code. Sync so flag
|
|
37
36
|
// parsing in cli/common.ts can fall back to it at startup.
|
|
38
37
|
function readStoredCurrency() {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
if (!existsSync(lock))
|
|
38
|
+
const file = tersioSettingsFile();
|
|
39
|
+
if (!existsSync(file))
|
|
42
40
|
return DEFAULT_CURRENCY;
|
|
43
41
|
try {
|
|
44
|
-
const parsed = JSON.parse(readFileSync(
|
|
45
|
-
if (!parsed || typeof parsed !== 'object'
|
|
42
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
43
|
+
if (!parsed || typeof parsed !== 'object')
|
|
46
44
|
return DEFAULT_CURRENCY;
|
|
47
|
-
const
|
|
48
|
-
if (!entry || typeof entry !== 'object')
|
|
49
|
-
return DEFAULT_CURRENCY;
|
|
50
|
-
const raw = entry.currency;
|
|
45
|
+
const raw = parsed.currency;
|
|
51
46
|
return typeof raw === 'string' && isCurrencyCode(raw.trim().toUpperCase())
|
|
52
47
|
? raw.trim().toUpperCase()
|
|
53
48
|
: DEFAULT_CURRENCY;
|
|
@@ -68,7 +63,8 @@ const CURRENCY_META = {
|
|
|
68
63
|
CAD: { symbol: 'C$', decimals: 2 },
|
|
69
64
|
INR: { symbol: '₹', decimals: 2 },
|
|
70
65
|
};
|
|
71
|
-
|
|
66
|
+
// Exported so a test can hold it against the Dashboard's copy of the same table.
|
|
67
|
+
export const FX_SNAPSHOT_RATES = {
|
|
72
68
|
USD: 1,
|
|
73
69
|
PHP: 58.7,
|
|
74
70
|
EUR: 0.92,
|
package/cli/dashboard.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Note: no Promise.withResolvers here — engines still allow Node 20.12,
|
|
4
|
-
// which lacks it; the listening server itself keeps the process alive.
|
|
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.
|
|
5
3
|
import { spawn } from 'node:child_process';
|
|
6
4
|
import { execFileSync } from 'node:child_process';
|
|
7
5
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -13,8 +11,10 @@ import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usa
|
|
|
13
11
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
14
12
|
import { summarizeUsage } from './usage.js';
|
|
15
13
|
import { isCurrencyCode } from './currency.js';
|
|
16
|
-
import {
|
|
14
|
+
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
17
15
|
import { storedProfile, writePluginSettings } from './profile.js';
|
|
16
|
+
import { PACKAGE_NAME } from './common.js';
|
|
17
|
+
import { resolveRtkBinary } from '../extensions/lib/utils.js';
|
|
18
18
|
const DASHBOARD_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard', 'dist');
|
|
19
19
|
const DASHBOARD_INDEX = path.join(DASHBOARD_DIST, 'index.html');
|
|
20
20
|
const DASHBOARD_BRAND = path.join(DASHBOARD_DIST, 'brand.webp');
|
|
@@ -50,10 +50,9 @@ async function brandDataUri() {
|
|
|
50
50
|
function dataJson() {
|
|
51
51
|
return JSON.stringify(summarizeUsage(readUsage()));
|
|
52
52
|
}
|
|
53
|
-
// Local-only health
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const names = process.platform === 'win32' ? ['omp.cmd', 'omp.exe', 'omp.bat', 'omp'] : ['omp'];
|
|
53
|
+
// Local-only health: find each agent on PATH, run `--version`, report both.
|
|
54
|
+
function binOnPath(bin) {
|
|
55
|
+
const names = process.platform === 'win32' ? [`${bin}.cmd`, `${bin}.exe`, `${bin}.bat`, bin] : [bin];
|
|
57
56
|
for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
58
57
|
for (const name of names) {
|
|
59
58
|
const candidate = path.join(dir, name);
|
|
@@ -66,15 +65,28 @@ function ompPath() {
|
|
|
66
65
|
}
|
|
67
66
|
return null;
|
|
68
67
|
}
|
|
69
|
-
function
|
|
68
|
+
function ompPath() {
|
|
69
|
+
return binOnPath('omp');
|
|
70
|
+
}
|
|
71
|
+
function piPath() {
|
|
72
|
+
return binOnPath('pi');
|
|
73
|
+
}
|
|
74
|
+
// omp prints `omp/18.4.1`, pi prints a bare `0.87.1`; both read as name/version.
|
|
75
|
+
function versionLabel(bin, raw) {
|
|
76
|
+
const out = raw.trim();
|
|
77
|
+
return out.includes('/') ? out : `${bin}/${out}`;
|
|
78
|
+
}
|
|
79
|
+
function agentVersion(bin) {
|
|
70
80
|
if (!bin)
|
|
71
81
|
return null;
|
|
72
82
|
try {
|
|
73
83
|
if (process.platform === 'win32') {
|
|
74
84
|
const exe = process.env.ComSpec || 'cmd.exe';
|
|
75
|
-
|
|
85
|
+
const out = execFileSync(exe, ['/d', '/s', '/c', bin, '--version'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
86
|
+
return out.trim() ? versionLabel(path.basename(bin), out) : null;
|
|
76
87
|
}
|
|
77
|
-
|
|
88
|
+
const out = execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
89
|
+
return out.trim() ? versionLabel(path.basename(bin), out) : null;
|
|
78
90
|
}
|
|
79
91
|
catch {
|
|
80
92
|
return null;
|
|
@@ -99,17 +111,20 @@ function ompDefaultModel() {
|
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
function healthJson() {
|
|
102
|
-
const rtkBin =
|
|
103
|
-
const rtkPresent =
|
|
114
|
+
const rtkBin = resolveRtkBinary();
|
|
115
|
+
const rtkPresent = rtkBin !== null;
|
|
104
116
|
const detectedOmpPath = ompPath();
|
|
117
|
+
const detectedPiPath = piPath();
|
|
105
118
|
return JSON.stringify({
|
|
106
119
|
tersio: PACKAGE_VERSION,
|
|
107
120
|
node: process.version,
|
|
108
121
|
platform: `${process.platform}/${process.arch}`,
|
|
109
|
-
omp:
|
|
122
|
+
omp: agentVersion(detectedOmpPath),
|
|
110
123
|
ompPath: detectedOmpPath,
|
|
124
|
+
pi: agentVersion(detectedPiPath),
|
|
125
|
+
piPath: detectedPiPath,
|
|
111
126
|
provider: ompDefaultModel(),
|
|
112
|
-
rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin },
|
|
127
|
+
rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin || 'not found in PATH' },
|
|
113
128
|
home: tersioHomePath(),
|
|
114
129
|
});
|
|
115
130
|
}
|
|
@@ -123,8 +138,7 @@ const DIAG_TTL_MS = {
|
|
|
123
138
|
monthly: 30 * 24 * 3600 * 1000,
|
|
124
139
|
};
|
|
125
140
|
function diagPaths() {
|
|
126
|
-
//
|
|
127
|
-
// so test runs never touch the real ~/.tersio/diag.json.
|
|
141
|
+
// Keep the report beside the overridden DB so tests never touch the real one.
|
|
128
142
|
const dbOverride = process.env.TERSIO_USAGE_DB;
|
|
129
143
|
if (dbOverride)
|
|
130
144
|
return { report: path.join(path.dirname(dbOverride), 'diag.json') };
|
|
@@ -183,29 +197,39 @@ function relAge(ms) {
|
|
|
183
197
|
}
|
|
184
198
|
function computeDoctorRows() {
|
|
185
199
|
const rows = [];
|
|
186
|
-
const
|
|
187
|
-
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
200
|
+
const rtkBin = resolveRtkBinary();
|
|
188
201
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
202
|
+
const tersioPluginDir = path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'));
|
|
189
203
|
const ok = (p) => existsSync(p);
|
|
204
|
+
const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
|
|
190
205
|
const ext = (label, p) => {
|
|
191
206
|
rows.push({ label, ok: ok(p), detail: ok(p) ? 'installed' : p, group: 'Extensions' });
|
|
192
207
|
};
|
|
193
208
|
const addon = (label, present, detail, group = 'Add-ons') => {
|
|
194
209
|
rows.push({ label, ok: present, detail, group });
|
|
195
210
|
};
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
let explicitEntries = [];
|
|
212
|
+
try {
|
|
213
|
+
explicitEntries = readFileSync(configPath, 'utf8').split('\n')
|
|
214
|
+
.map((line) => line.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, ''))
|
|
215
|
+
.filter((line) => line.startsWith('/') || line.startsWith('.'));
|
|
216
|
+
}
|
|
217
|
+
catch { /* missing config is reported by extension rows */ }
|
|
218
|
+
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
219
|
+
addon('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : 'ok', 'Extensions');
|
|
220
|
+
const retiredReinforcement = explicitEntries.filter((entry) => entry.endsWith('/shared/mode-reinforcement.ts')).length;
|
|
221
|
+
addon('No retired reinforcement', retiredReinforcement === 0, retiredReinforcement ? `${retiredReinforcement} registration(s)` : 'ok', 'Extensions');
|
|
222
|
+
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
|
|
223
|
+
ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
|
|
198
224
|
ext('Ponytail extension', path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
|
|
199
|
-
const rule = path.join(
|
|
225
|
+
const rule = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'rule.md');
|
|
200
226
|
const ruleAge = ageStr(rule);
|
|
201
227
|
const ruleAt = absTime(rule);
|
|
202
228
|
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);
|
|
207
|
-
const wiring = path.join(extDir, 'rtk.ts');
|
|
208
|
-
addon('RTK OMP wiring (rtk.ts)', ok(wiring), ok(wiring) ? 'loaded' : wiring);
|
|
229
|
+
const rtkVer = rtkBin ? rtkVersion(rtkBin) : null;
|
|
230
|
+
const rtkAge = rtkBin ? ageStr(rtkBin) : null;
|
|
231
|
+
const rtkAt = rtkBin ? absTime(rtkBin) : null;
|
|
232
|
+
addon('RTK binary', rtkVer !== null, rtkVer && rtkAge && rtkAt ? `${rtkVer} (updated ${rtkAge} · ${rtkAt})` : rtkBin || 'not found in PATH');
|
|
209
233
|
let ponytailVer = null;
|
|
210
234
|
try {
|
|
211
235
|
ponytailVer = JSON.parse(readFileSync(ponytailPkg, 'utf8')).version ?? null;
|
|
@@ -222,10 +246,8 @@ function computeDoctorRows() {
|
|
|
222
246
|
addon('Prices feed', pricesAge !== null, pricesAge && pricesAt ? `(pulled ${pricesAge} · ${pricesAt})` : prices, 'Usage');
|
|
223
247
|
return rows;
|
|
224
248
|
}
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// Labels retired from the report. A saved report containing any of them
|
|
228
|
-
// is stale by definition and always recomputes.
|
|
249
|
+
// Recompute when forced, missing, or past the schedule; a report holding a
|
|
250
|
+
// retired label is stale by definition.
|
|
229
251
|
const RETIRED_DIAG_LABELS = new Set([
|
|
230
252
|
'OMP CLI',
|
|
231
253
|
'Tersio CLI',
|
|
@@ -240,13 +262,11 @@ const RETIRED_DIAG_LABELS = new Set([
|
|
|
240
262
|
function isRetiredReport(report) {
|
|
241
263
|
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
242
264
|
}
|
|
243
|
-
//
|
|
244
|
-
// than the schedule. Otherwise return the saved report.
|
|
265
|
+
// Recompute when forced, missing, retired, or past the schedule.
|
|
245
266
|
function getDoctorReport(force) {
|
|
246
267
|
const saved = readDiagReport();
|
|
247
268
|
const ttl = saved ? DIAG_TTL_MS[saved.schedule] : 0;
|
|
248
|
-
// Manual
|
|
249
|
-
// report until stale. Otherwise a retained snapshot hides new rows.
|
|
269
|
+
// Manual recomputes on open; a dated schedule reuses the saved report.
|
|
250
270
|
if (!force && saved && !isRetiredReport(saved) && ttl > 0 && Date.now() - saved.checkedAt < ttl)
|
|
251
271
|
return saved;
|
|
252
272
|
const report = { rows: computeDoctorRows(), checkedAt: Date.now(), schedule: saved?.schedule ?? 'manual' };
|
|
@@ -262,10 +282,8 @@ function setDiagSchedule(schedule) {
|
|
|
262
282
|
function readDiagSchedule() {
|
|
263
283
|
return readDiagReport()?.schedule ?? 'manual';
|
|
264
284
|
}
|
|
265
|
-
// Persist the
|
|
266
|
-
//
|
|
267
|
-
// browser's localStorage alone cannot survive a restart. The stored default
|
|
268
|
-
// feeds data.json and `tersio usage` on the next run.
|
|
285
|
+
// Persist the picker choice: every run serves a fresh port, so localStorage
|
|
286
|
+
// alone cannot survive a restart.
|
|
269
287
|
async function saveDashboardCurrency(raw) {
|
|
270
288
|
if (typeof raw !== 'string')
|
|
271
289
|
return null;
|
|
@@ -289,21 +307,30 @@ function openBrowser(url) {
|
|
|
289
307
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
290
308
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
291
309
|
}
|
|
292
|
-
//
|
|
293
|
-
//
|
|
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.
|
|
310
|
+
// Replacers throughout: session data holds `$'` (shell quoting) and literal
|
|
311
|
+
// `</script>`, which String.replace would mangle.
|
|
299
312
|
function escapeInline(json) {
|
|
300
313
|
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
301
314
|
}
|
|
315
|
+
// Our own JSON.stringify output, so a parse failure means a bad value: name it.
|
|
316
|
+
function parseOwnJson(json, field) {
|
|
317
|
+
let parsed;
|
|
318
|
+
try {
|
|
319
|
+
parsed = JSON.parse(json);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
throw new Error(`dashboard snapshot ${field} is not valid JSON`);
|
|
323
|
+
}
|
|
324
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
325
|
+
throw new Error(`dashboard snapshot ${field} is not an object`);
|
|
326
|
+
}
|
|
327
|
+
return parsed;
|
|
328
|
+
}
|
|
302
329
|
async function exportDashboard(exportFile) {
|
|
303
330
|
requireDashboardBundle();
|
|
304
331
|
const [bundle, icon] = await Promise.all([readSegment(DASHBOARD_INDEX), brandDataUri()]);
|
|
305
332
|
const inline = bundle
|
|
306
|
-
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({ data:
|
|
333
|
+
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({ data: parseOwnJson(dataJson(), 'usage data'), health: parseOwnJson(healthJson(), 'health'), doctor: getDoctorReport(false) }))};`)
|
|
307
334
|
.replace(/href="brand\.webp"/g, () => `href="${icon}"`)
|
|
308
335
|
.replace(/src="brand\.webp"/g, () => `src="${icon}"`)
|
|
309
336
|
.replace('fetch("brand.webp")', () => `Promise.resolve({ ok: true, blob: async () => new Blob([atob("${icon.split(',')[1]}")], { type: "image/webp" }) })`);
|