@krtclcdy/tersio 2.25.5 → 2.25.6
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 +17 -36
- package/dashboard/dist/index.html +15 -15
- package/dist/cli/backups.js +7 -14
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +28 -12
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +66 -35
- package/dist/cli/doctor-fix.js +204 -19
- package/dist/cli/doctor.js +226 -39
- package/dist/cli/hosts.js +25 -5
- package/dist/cli/install.js +144 -48
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +87 -7
- package/dist/cli/profile.js +13 -8
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +3 -3
- package/dist/cli/update.js +6 -7
- package/dist/cli/usage.js +6 -9
- package/dist/extensions/caveman/index.js +4 -2
- package/dist/extensions/caveman/rule-megacave.md +5 -3
- package/dist/extensions/caveman/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +55 -45
- package/dist/extensions/combo/index.js +62 -21
- package/dist/extensions/commands/index.js +1 -1
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/rtk/filter.js +0 -1
- package/dist/extensions/rtk/session.js +4 -4
- package/dist/extensions/server.js +90 -6
- package/dist/extensions/shared/agent-stats.js +214 -278
- package/dist/extensions/shared/brand-marks.js +33 -34
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +11 -6
- package/dist/extensions/shared/pricing.js +8 -16
- package/dist/extensions/shared/rtk-gain.js +4 -9
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +168 -0
- package/dist/extensions/shared/status.js +8 -8
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +53 -41
- package/dist/extensions/shared/usage-store.js +111 -52
- package/dist/extensions/shared/utils.js +5 -5
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/updater/index.js +3 -6
- package/dist/tersio.js +8 -3
- package/extensions/caveman/index.ts +4 -2
- package/extensions/caveman/rule-megacave.md +5 -3
- package/extensions/caveman/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +55 -45
- package/extensions/combo/index.ts +62 -19
- package/extensions/commands/index.ts +1 -1
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/rtk/filter.ts +0 -1
- package/extensions/rtk/session.ts +4 -4
- package/extensions/server.ts +72 -7
- package/extensions/shared/agent-stats.ts +226 -278
- package/extensions/shared/brand-marks.ts +34 -34
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +12 -6
- package/extensions/shared/pricing.ts +10 -26
- package/extensions/shared/rtk-gain.ts +4 -9
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +168 -0
- package/extensions/shared/status.ts +9 -8
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +60 -47
- package/extensions/shared/usage-store.ts +120 -52
- package/extensions/shared/utils.ts +5 -5
- package/extensions/timing/index.ts +45 -0
- package/extensions/updater/index.ts +3 -8
- package/package.json +17 -8
package/dist/cli/backups.js
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
// Data snapshots:
|
|
2
|
-
//
|
|
3
|
-
// A snapshot is a directory with a manifest, not a single file, so one copy holds the whole set.
|
|
4
|
-
// Older installs wrote a lone `usage-<stamp>.db`; those still list and restore, over the ledger alone.
|
|
1
|
+
// Data snapshots: a manifest directory, not one file; older lone `usage-<stamp>.db` installs still restore.
|
|
5
2
|
import { promises as fs } from 'node:fs';
|
|
6
3
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
4
|
import path from 'node:path';
|
|
5
|
+
import { sqliteExec } from '../extensions/shared/sqlite.js';
|
|
9
6
|
import { agentDbPath, statsDbPath } from '../extensions/shared/agent-stats.js';
|
|
10
7
|
import { rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
11
8
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
12
9
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
13
10
|
import { resetMarkerPath } from '../extensions/shared/usage-ledger.js';
|
|
14
11
|
import { tersioHome } from '../extensions/shared/utils.js';
|
|
15
|
-
/** Snapshots kept on disk
|
|
16
|
-
* the pane scrolls instead of showing the whole history at once. */
|
|
12
|
+
/** Snapshots kept on disk, oldest pruned first; six fills past one screen so the pane scrolls. */
|
|
17
13
|
export const BACKUP_KEEP = 6;
|
|
18
|
-
/** Where a snapshot's files live. */
|
|
19
14
|
export function backupDir() {
|
|
20
15
|
return path.join(tersioHome(), 'backups');
|
|
21
16
|
}
|
|
22
|
-
/** A snapshot directory's manifest, read back out with its files. */
|
|
23
17
|
function manifestPath(dir) {
|
|
24
18
|
return path.join(dir, 'manifest.json');
|
|
25
19
|
}
|
|
@@ -90,7 +84,6 @@ export function listDataBackups() {
|
|
|
90
84
|
}
|
|
91
85
|
return [...dirs, ...legacy].toSorted((a, b) => b.mtime - a.mtime);
|
|
92
86
|
}
|
|
93
|
-
/** The files a snapshot directory holds, read from its manifest. */
|
|
94
87
|
function readManifest(dir) {
|
|
95
88
|
try {
|
|
96
89
|
const raw = JSON.parse(readFileSync(manifestPath(dir), 'utf8'));
|
|
@@ -120,13 +113,13 @@ function totalOf(dir) {
|
|
|
120
113
|
/** Put the sqlite ledger's write-ahead log back in the main file, so a copy is one whole database. */
|
|
121
114
|
function checkpointLedger() {
|
|
122
115
|
try {
|
|
123
|
-
|
|
116
|
+
sqliteExec(usageDbPath(), 'PRAGMA wal_checkpoint(TRUNCATE);');
|
|
124
117
|
}
|
|
125
118
|
catch {
|
|
126
|
-
/* no
|
|
119
|
+
/* no SQLite on this host, or no ledger: the plain file copy is the best available */
|
|
127
120
|
}
|
|
128
121
|
}
|
|
129
|
-
/** Take a snapshot
|
|
122
|
+
/** Take a snapshot: one copy of every existing file under a stamp dir, keeping the newest {@link BACKUP_KEEP}. */
|
|
130
123
|
export async function createDataBackup() {
|
|
131
124
|
// The stamp keeps milliseconds: two snapshots in the same second must not land in one directory.
|
|
132
125
|
const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '');
|
|
@@ -159,7 +152,7 @@ function resolveBackup(name) {
|
|
|
159
152
|
const found = listDataBackups().find((entry) => entry.name === name);
|
|
160
153
|
return found ?? null;
|
|
161
154
|
}
|
|
162
|
-
/**
|
|
155
|
+
/** Restore: files the snapshot holds overwrite their live source; files it lacks stay put, so nothing is lost. */
|
|
163
156
|
export async function restoreDataBackup(name) {
|
|
164
157
|
const found = resolveBackup(name);
|
|
165
158
|
if (!found)
|
package/dist/cli/banner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli/banner.ts — mini pixel-scissor welcome. TTY-only
|
|
1
|
+
// cli/banner.ts — mini pixel-scissor welcome. TTY-only, skipped on --dry-run; piped output stays byte-identical.
|
|
2
2
|
import { intro as clackIntro, note as clackNote } from '@clack/prompts';
|
|
3
3
|
import { PACKAGE_VERSION, dryRun } from './common.js';
|
|
4
4
|
function tty() {
|
|
@@ -81,9 +81,10 @@ function printWelcome() {
|
|
|
81
81
|
'tersio update refresh CLI + add-ons',
|
|
82
82
|
'tersio doctor verify the install',
|
|
83
83
|
];
|
|
84
|
-
// Below 26 rows the note scrolls off the top and slices the art
|
|
84
|
+
// Below 26 rows the note scrolls off the top and slices the art, and the screen must also hold the
|
|
85
|
+
// update prompt and every option, so the tips take one line there.
|
|
85
86
|
if (process.stdout.rows < 26) {
|
|
86
|
-
clackNote(
|
|
87
|
+
clackNote(` ${tips.join(' · ')}`);
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
const art = bannerLines();
|
package/dist/cli/common.js
CHANGED
|
@@ -19,14 +19,15 @@ const PACKAGE_BIN = 'tersio';
|
|
|
19
19
|
const { version: PACKAGE_VERSION } = createRequire(import.meta.url)(path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'package.json'));
|
|
20
20
|
const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'megacave-lite', 'megacave-full', 'megacave-ultra']);
|
|
21
21
|
const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra']);
|
|
22
|
+
const PSTACK_DEFAULTS = new Set(['off', 'on']);
|
|
22
23
|
const RTK_DEFAULTS = new Set(['on', 'off']);
|
|
23
24
|
const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
24
|
-
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts
|
|
25
|
+
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts, so the combo preset implies all four modes.
|
|
25
26
|
const COMBO_PRESET_MODES = {
|
|
26
|
-
off: { caveman: 'off', rtk: false, ponytail: 'off' },
|
|
27
|
-
medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
|
|
28
|
-
balanced: { caveman: 'full', rtk: true, ponytail: 'full' },
|
|
29
|
-
max: { caveman: 'ultra', rtk: true, ponytail: 'ultra' },
|
|
27
|
+
off: { caveman: 'off', rtk: false, ponytail: 'off', pstack: 'off' },
|
|
28
|
+
medium: { caveman: 'lite', rtk: true, ponytail: 'lite', pstack: 'on' },
|
|
29
|
+
balanced: { caveman: 'full', rtk: true, ponytail: 'full', pstack: 'on' },
|
|
30
|
+
max: { caveman: 'ultra', rtk: true, ponytail: 'ultra', pstack: 'on' },
|
|
30
31
|
};
|
|
31
32
|
const COMBO_DEFAULTS = new Set(Object.keys(COMBO_PRESET_MODES));
|
|
32
33
|
function parseEnum(value, valid, flag) {
|
|
@@ -46,12 +47,11 @@ function flagValue(name) {
|
|
|
46
47
|
const prefix = `${name}=`;
|
|
47
48
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
const COMMANDS = { install: true, update: true, doctor: true, uninstall: true, usage: true, dashboard: true, reset: true, settings: true, version: true, help: true };
|
|
50
|
+
const COMMANDS = { install: true, reinstall: true, update: true, doctor: true, uninstall: true, usage: true, dashboard: true, reset: true, settings: true, version: true, help: true };
|
|
51
51
|
const args = process.argv.slice(2);
|
|
52
52
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
53
53
|
const command = commandArg?.toLowerCase() || null;
|
|
54
|
-
// `tersio settings diagnosis` jumps to one prompt
|
|
54
|
+
// `tersio settings diagnosis` jumps to one prompt. Index-based, so flag values never count as positionals.
|
|
55
55
|
const settingArg = (() => {
|
|
56
56
|
const at = args.indexOf(commandArg ?? '');
|
|
57
57
|
const next = at >= 0 ? args[at + 1] : undefined;
|
|
@@ -61,6 +61,7 @@ const settingArg = (() => {
|
|
|
61
61
|
})();
|
|
62
62
|
const unknownCommand = command !== null && !COMMANDS[command];
|
|
63
63
|
const install = command === 'install';
|
|
64
|
+
const reinstall = command === 'reinstall';
|
|
64
65
|
const update = command === 'update';
|
|
65
66
|
const showVersion = command === 'version' || args.includes('--version') || args.includes('-v');
|
|
66
67
|
const showHelp = command === 'help' || args.includes('--help') || args.includes('-h');
|
|
@@ -96,9 +97,10 @@ if (legacyScope !== null && legacyScope !== 'user') {
|
|
|
96
97
|
const comboDefaultFlag = parseEnum(flagValue('--combo-default'), COMBO_DEFAULTS, '--combo-default');
|
|
97
98
|
const cavemanDefaultFlag = parseEnum(flagValue('--caveman-default'), CAVEMAN_DEFAULTS, '--caveman-default');
|
|
98
99
|
const ponytailDefaultFlag = parseEnum(flagValue('--ponytail-default'), PONYTAIL_DEFAULTS, '--ponytail-default');
|
|
100
|
+
const pstackDefaultFlag = parseEnum(flagValue('--pstack-default'), PSTACK_DEFAULTS, '--pstack-default');
|
|
99
101
|
const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rtk-default');
|
|
100
102
|
const diagScheduleFlag = parseEnum(flagValue('--diag-schedule'), DIAG_SCHEDULES, '--diag-schedule');
|
|
101
|
-
const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
|
|
103
|
+
const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag]
|
|
102
104
|
.some((flag) => flag !== undefined);
|
|
103
105
|
// Repeatable `--subagent-marker`; overrides the built-in host prompt marker.
|
|
104
106
|
const subagentMarkerFlags = args
|
|
@@ -138,7 +140,7 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
138
140
|
console.log(` [write] ${dest}`);
|
|
139
141
|
return true;
|
|
140
142
|
}
|
|
141
|
-
// omp ships 'extensions: null'
|
|
143
|
+
// omp ships 'extensions: null'; normalize it to a mapping key first, or YAML rejects the list items.
|
|
142
144
|
const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
|
|
143
145
|
const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
|
|
144
146
|
function normalizeExtensionsKey(lines) {
|
|
@@ -160,7 +162,7 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
160
162
|
const line = ` - ${normalizedPath}`;
|
|
161
163
|
const raw = await readTextIfExists(configPath);
|
|
162
164
|
let lines = (raw || '').split('\n');
|
|
163
|
-
// Drop legacy compiled twins (
|
|
165
|
+
// Drop legacy compiled twins (.js beside the name): OMP must never load both. Matched on the trailing path.
|
|
164
166
|
if (normalizedPath.endsWith('.ts')) {
|
|
165
167
|
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
166
168
|
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
@@ -239,6 +241,12 @@ function readPonytailConfig() {
|
|
|
239
241
|
: path.join(HOME, '.config', 'ponytail');
|
|
240
242
|
return { dir, path: path.join(dir, 'config.json') };
|
|
241
243
|
}
|
|
244
|
+
/** This package's ponytail range, or null; repair pins it, since npm installs the published range. */
|
|
245
|
+
async function bundledPonytailSpec() {
|
|
246
|
+
const root = findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
247
|
+
const text = await readTextIfExists(path.join(root, 'package.json'));
|
|
248
|
+
return parseJsonObject(text)?.dependencies?.['@dietrichgebert/ponytail'] ?? null;
|
|
249
|
+
}
|
|
242
250
|
function parseJsonObject(text) {
|
|
243
251
|
if (!text)
|
|
244
252
|
return null;
|
|
@@ -280,6 +288,14 @@ async function ensurePonytailConfigValue(key, value, options = {}) {
|
|
|
280
288
|
return true;
|
|
281
289
|
}, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
|
|
282
290
|
}
|
|
291
|
+
// npm prefers a nested copy over the pin when the plugin's range excludes it; only `overrides` reaches it.
|
|
292
|
+
function ponytailOverride(pkg) {
|
|
293
|
+
const overrides = pkg.overrides;
|
|
294
|
+
if (typeof overrides !== 'object' || overrides === null)
|
|
295
|
+
return null;
|
|
296
|
+
const value = overrides['@dietrichgebert/ponytail'];
|
|
297
|
+
return typeof value === 'string' ? value : null;
|
|
298
|
+
}
|
|
283
299
|
// Read plugins/package.json tolerantly; a corrupt or missing file starts fresh with installer-managed defaults.
|
|
284
300
|
async function readPluginsPackage(pkgPath) {
|
|
285
301
|
const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
|
|
@@ -300,4 +316,4 @@ function relTime(ageMs) {
|
|
|
300
316
|
const weeks = Math.floor(days / 7);
|
|
301
317
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
302
318
|
}
|
|
303
|
-
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, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
|
|
319
|
+
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, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, 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, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
|
package/dist/cli/currency.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli/currency.ts — USD-base
|
|
1
|
+
// cli/currency.ts — USD-base helpers on offline snapshot rates. Same 10 currencies as the dashboard's live figures.
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
4
4
|
const CURRENCY_CODES = [
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Dashboard server + usage APIs. Binds 127.0.0.1; --export writes a file. No Promise.withResolvers (Node 20.12).
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -11,11 +11,11 @@ import { isCurrencyCode } from './currency.js';
|
|
|
11
11
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
12
12
|
import { BACKUP_SCHEDULES, formatCliStatus, storedProfile, writePluginSettings } from './profile.js';
|
|
13
13
|
import { normalizeComboLevel } from '../extensions/shared/session-state.js';
|
|
14
|
-
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS } from './common.js';
|
|
14
|
+
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS } from './common.js';
|
|
15
15
|
import { PACKAGE_NAME } from './common.js';
|
|
16
|
-
import { findPackageRoot, resolveRtkBinary } from '../extensions/shared/utils.js';
|
|
16
|
+
import { findPackageRoot, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
|
|
17
17
|
import { BACKUP_KEEP, backupDir, backupDiskBytes, createDataBackup, dataSources, deleteDataBackup, listDataBackups, restoreDataBackup } from './backups.js';
|
|
18
|
-
import { readBackupSchedule } from '../extensions/shared/usage-store.js';
|
|
18
|
+
import { readBackupSchedule, syncUsageDb } from '../extensions/shared/usage-store.js';
|
|
19
19
|
import { agentDbPath, emptyRequestEntry, isAgentView, isRangeKey, readAgentRequestEntry, readAgentStats, readSessionTrace, statsDbPath } from '../extensions/shared/agent-stats.js';
|
|
20
20
|
const DASHBOARD_DIST = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'dashboard', 'dist');
|
|
21
21
|
const DASHBOARD_INDEX = path.join(DASHBOARD_DIST, 'index.html');
|
|
@@ -51,9 +51,12 @@ async function brandDataUri() {
|
|
|
51
51
|
}
|
|
52
52
|
/** The range a page opens on, matching the reference dashboard. */
|
|
53
53
|
const DEFAULT_AGENT_RANGE = '24h';
|
|
54
|
-
/**
|
|
54
|
+
/** Poll memo TTL: every poll re-reads the aggregate, so a repeat poll reuses the last answer. */
|
|
55
55
|
const AGENT_REFRESH_MS = 5000;
|
|
56
|
+
/** A sync walks every transcript, so it runs at most this often even while a poll outruns it. */
|
|
57
|
+
const AGENT_SYNC_MS = 15_000;
|
|
56
58
|
let agentMemo = null;
|
|
59
|
+
let lastSyncAt = 0;
|
|
57
60
|
function fileStamp(file) {
|
|
58
61
|
try {
|
|
59
62
|
const stat = statSync(file);
|
|
@@ -63,15 +66,25 @@ function fileStamp(file) {
|
|
|
63
66
|
return '-';
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
/**
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
+
/** Both sources: the usage store carries every host's rows, agent.db only omp's quota snapshots. */
|
|
70
|
+
function storeStamp() {
|
|
71
|
+
return `${fileStamp(statsDbPath())}|${fileStamp(agentDbPath())}`;
|
|
72
|
+
}
|
|
73
|
+
/** Syncs before reading past the throttle; without it a page misses pi and OpenCode rows. */
|
|
74
|
+
function agentStats(range, view = 'all', offset = 0) {
|
|
69
75
|
const held = agentMemo;
|
|
70
|
-
if (held && held.range === range && held.view === view && held.
|
|
76
|
+
if (held && held.range === range && held.view === view && held.offset === offset && held.stamp === storeStamp() && Date.now() - held.at < AGENT_REFRESH_MS) {
|
|
71
77
|
return held.value;
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
if (Date.now() - lastSyncAt >= AGENT_SYNC_MS) {
|
|
80
|
+
lastSyncAt = Date.now();
|
|
81
|
+
try {
|
|
82
|
+
syncUsageDb();
|
|
83
|
+
}
|
|
84
|
+
catch { /* a store that will not write still reads the rows it already holds */ }
|
|
85
|
+
}
|
|
86
|
+
const value = readAgentStats(range, view, Date.now(), offset);
|
|
87
|
+
agentMemo = { range, view, offset, stamp: storeStamp(), at: Date.now(), value };
|
|
75
88
|
return value;
|
|
76
89
|
}
|
|
77
90
|
/** `?range=` on any of the omp routes; an unknown value falls back to the default. */
|
|
@@ -86,8 +99,14 @@ function viewFrom(url) {
|
|
|
86
99
|
const raw = queryParam(url, 'view');
|
|
87
100
|
return isAgentView(raw) ? raw : 'all';
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
/** `?offset=` in whole rows, floored; a later page starts further back instead of clamping at the cap. */
|
|
103
|
+
function offsetFrom(url) {
|
|
104
|
+
const raw = queryParam(url, 'offset');
|
|
105
|
+
const value = raw === null ? 0 : Number(raw);
|
|
106
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
107
|
+
}
|
|
108
|
+
function agentJson(range, view, offset) {
|
|
109
|
+
return JSON.stringify({ range, view, offset, agent: agentStats(range, view, offset) });
|
|
91
110
|
}
|
|
92
111
|
// Runs outside any agent session, so it reports the persisted defaults.
|
|
93
112
|
async function statusJson() {
|
|
@@ -98,6 +117,7 @@ async function statusJson() {
|
|
|
98
117
|
caveman: profile.cavemanDefault,
|
|
99
118
|
rtk: profile.rtkDefault ? 'on' : 'off',
|
|
100
119
|
ponytail: profile.ponytailDefault,
|
|
120
|
+
pstack: profile.pstackDefault,
|
|
101
121
|
});
|
|
102
122
|
}
|
|
103
123
|
// Local-only health: find each agent on PATH, run `--version`, report both.
|
|
@@ -282,18 +302,29 @@ function computeDoctorRows() {
|
|
|
282
302
|
}
|
|
283
303
|
catch { /* missing config is reported by extension rows */ }
|
|
284
304
|
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
285
|
-
addon('Unique config registrations',
|
|
286
|
-
// A listed extension whose file is gone makes the host warn on every load, so surface it
|
|
305
|
+
addon('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : 'ok', 'Extensions');
|
|
306
|
+
// A listed extension whose file is gone makes the host warn on every load, so surface it here.
|
|
287
307
|
const dangling = explicitEntries
|
|
288
308
|
.filter((entry) => entry.includes(`extensions${path.sep}`) || entry.includes('extensions/'))
|
|
289
309
|
.filter((entry) => !ok(path.isAbsolute(entry) ? entry : path.resolve(OMP_AGENT_DIR, entry)));
|
|
290
310
|
addon('Extension files present', dangling.length === 0, dangling.length ? dangling.join(', ') : 'ok', 'Extensions');
|
|
291
|
-
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman
|
|
292
|
-
ext('
|
|
293
|
-
|
|
311
|
+
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman', 'index.ts'));
|
|
312
|
+
ext('pstack extension', path.join(tersioPluginDir, 'extensions', 'pstack', 'index.ts'));
|
|
313
|
+
ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk', 'session.ts'));
|
|
314
|
+
const rule = path.join(tersioPluginDir, 'extensions', 'caveman', 'rule.md');
|
|
294
315
|
const ruleAge = ageStr(rule);
|
|
295
316
|
const ruleAt = absTime(rule);
|
|
296
317
|
addon('Caveman rule', ruleAge !== null, ruleAge && ruleAt ? `(updated ${ruleAge} · ${ruleAt})` : rule);
|
|
318
|
+
// The pstack body is a Tersio fork with no remote, so this row proves it is usable, not current.
|
|
319
|
+
const pstackRule = path.join(tersioPluginDir, 'extensions', 'pstack', 'rule.md');
|
|
320
|
+
let pstackProblem = 'missing — run: tersio install';
|
|
321
|
+
try {
|
|
322
|
+
pstackProblem = ruleBodyProblem(readFileSync(pstackRule, 'utf8'));
|
|
323
|
+
}
|
|
324
|
+
catch { /* missing stays */ }
|
|
325
|
+
const pstackAge = ageStr(pstackRule);
|
|
326
|
+
const pstackAt = absTime(pstackRule);
|
|
327
|
+
addon('pstack rule', pstackProblem === null, pstackProblem ?? `(updated ${pstackAge} · ${pstackAt})`);
|
|
297
328
|
const rtkVer = rtkBin ? rtkVersion(rtkBin) : null;
|
|
298
329
|
const rtkAge = rtkBin ? ageStr(rtkBin) : null;
|
|
299
330
|
const rtkAt = rtkBin ? absTime(rtkBin) : null;
|
|
@@ -314,7 +345,7 @@ function computeDoctorRows() {
|
|
|
314
345
|
addon('Prices feed', pricesAge !== null, pricesAge && pricesAt ? `(pulled ${pricesAge} · ${pricesAt})` : prices, 'Usage');
|
|
315
346
|
return rows;
|
|
316
347
|
}
|
|
317
|
-
//
|
|
348
|
+
// A report holding any of these labels is stale by definition.
|
|
318
349
|
const RETIRED_DIAG_LABELS = new Set([
|
|
319
350
|
'OMP CLI',
|
|
320
351
|
'Tersio CLI',
|
|
@@ -335,11 +366,10 @@ const RETIRED_DIAG_LABELS = new Set([
|
|
|
335
366
|
function isRetiredReport(report) {
|
|
336
367
|
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
337
368
|
}
|
|
338
|
-
// Recompute when forced, missing, retired, or past
|
|
369
|
+
// Recompute when forced, missing, retired, or past schedule; manual schedules recompute on open, dated ones reuse.
|
|
339
370
|
function getDoctorReport(force) {
|
|
340
371
|
const saved = readDiagReport();
|
|
341
372
|
const ttl = saved ? DIAG_TTL_MS[saved.schedule] : 0;
|
|
342
|
-
// Manual recomputes on open; a dated schedule reuses the saved report.
|
|
343
373
|
if (!force && saved && !isRetiredReport(saved) && ttl > 0 && Date.now() - saved.checkedAt < ttl)
|
|
344
374
|
return saved;
|
|
345
375
|
const report = { rows: computeDoctorRows(), checkedAt: Date.now(), schedule: saved?.schedule ?? 'manual' };
|
|
@@ -373,6 +403,12 @@ async function saveDefaults(body) {
|
|
|
373
403
|
return { ok: false, error: 'unknown ponytail level' };
|
|
374
404
|
profile.ponytailDefault = v;
|
|
375
405
|
}
|
|
406
|
+
if (body.pstackDefault !== undefined) {
|
|
407
|
+
const v = String(body.pstackDefault);
|
|
408
|
+
if (!PSTACK_DEFAULTS.has(v))
|
|
409
|
+
return { ok: false, error: 'unknown pstack level' };
|
|
410
|
+
profile.pstackDefault = v;
|
|
411
|
+
}
|
|
376
412
|
if (body.backupSchedule !== undefined) {
|
|
377
413
|
const v = String(body.backupSchedule);
|
|
378
414
|
if (!BACKUP_SCHEDULES.has(v))
|
|
@@ -392,6 +428,7 @@ async function saveDefaults(body) {
|
|
|
392
428
|
cavemanDefault: profile.cavemanDefault,
|
|
393
429
|
rtkDefault: profile.rtkDefault,
|
|
394
430
|
ponytailDefault: profile.ponytailDefault,
|
|
431
|
+
pstackDefault: profile.pstackDefault,
|
|
395
432
|
backupSchedule: profile.backupSchedule,
|
|
396
433
|
},
|
|
397
434
|
};
|
|
@@ -423,7 +460,7 @@ function openBrowser(url) {
|
|
|
423
460
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
424
461
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
425
462
|
}
|
|
426
|
-
// Replacers throughout: session data holds `$'` (shell quoting) and
|
|
463
|
+
// Replacers throughout: session data holds `$'` (shell quoting) and `</script>`, which String.replace would mangle.
|
|
427
464
|
function escapeInline(json) {
|
|
428
465
|
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
429
466
|
}
|
|
@@ -444,20 +481,13 @@ function parseOwnJson(json, field) {
|
|
|
444
481
|
async function exportDashboard(exportFile, currency) {
|
|
445
482
|
requireDashboardBundle();
|
|
446
483
|
const [bundle, icon] = await Promise.all([readSegment(DASHBOARD_INDEX), brandDataUri()]);
|
|
447
|
-
//
|
|
448
|
-
// `fetch("brand.webp")` depending on the bundler's quote choice. Match either, or the exported
|
|
449
|
-
// card silently loses its mark. `atob` returns one char per byte, and a Blob encodes a string
|
|
450
|
-
// as UTF-8, so the bytes go through a Uint8Array first: a webp is binary and every byte at or
|
|
451
|
-
// above 0x80 would otherwise double and corrupt the image.
|
|
484
|
+
// Minified: fetch reads `fetch(\`brand.webp\`)` or `fetch("brand.webp")`; a mismatch loses the card's mark.
|
|
452
485
|
const b64 = icon.split(',')[1] ?? '';
|
|
453
|
-
// atob yields one
|
|
454
|
-
// Uint8Array or every byte at or above 0x80 doubles and the webp comes out corrupt.
|
|
486
|
+
// atob yields one byte per char, so a Uint8Array is needed or bytes >= 0x80 double and corrupt the webp.
|
|
455
487
|
const brandBytes = `(() => { const c = atob(${JSON.stringify(b64)}); const v = new Uint8Array(c.length); for (let i = 0; i < c.length; i++) v[i] = c.charCodeAt(i); return v; })()`;
|
|
456
488
|
const inline = bundle
|
|
457
489
|
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({ currency, agent: agentStats(DEFAULT_AGENT_RANGE), health: parseOwnJson(healthJson(), 'health'), doctor: getDoctorReport(false) }))};`)
|
|
458
|
-
// A mark
|
|
459
|
-
// esbuild compiled with a backtick. Match any of `"`, `'`, and `` ` ``, or an exported file
|
|
460
|
-
// keeps a bare reference that file:// cannot resolve.
|
|
490
|
+
// A mark is an HTML attribute or JSX prop; match every quote or the file keeps an unresolvable reference.
|
|
461
491
|
.replace(/((?:src|href)[=:][ \t]*)([`"'])brand\.webp\2/g, (_all, head, quote) => `${head}${quote}${icon}${quote}`)
|
|
462
492
|
.replace(/fetch\(([`"])brand\.webp\1\)/g, () => `Promise.resolve({ ok: true, blob: async () => new Blob([${brandBytes}], { type: "image/webp" }) })`);
|
|
463
493
|
await fs.writeFile(exportFile, inline, 'utf8');
|
|
@@ -492,7 +522,7 @@ async function runDashboard(options) {
|
|
|
492
522
|
}
|
|
493
523
|
const action = String(body.action ?? '');
|
|
494
524
|
const name = String(body.name ?? '');
|
|
495
|
-
// A missing
|
|
525
|
+
// A missing SQLite engine, or a locked file, is a 500 with a message rather than a silent no-op.
|
|
496
526
|
try {
|
|
497
527
|
if (action === 'create') {
|
|
498
528
|
const made = await createDataBackup();
|
|
@@ -530,6 +560,7 @@ async function runDashboard(options) {
|
|
|
530
560
|
cavemanDefault: profile.cavemanDefault,
|
|
531
561
|
rtkDefault: profile.rtkDefault,
|
|
532
562
|
ponytailDefault: profile.ponytailDefault,
|
|
563
|
+
pstackDefault: profile.pstackDefault,
|
|
533
564
|
backupSchedule: profile.backupSchedule,
|
|
534
565
|
currency: options.currency,
|
|
535
566
|
}));
|
|
@@ -576,7 +607,7 @@ async function runDashboard(options) {
|
|
|
576
607
|
}
|
|
577
608
|
if (req.url === '/api/agent' || req.url?.startsWith('/api/agent?')) {
|
|
578
609
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
579
|
-
res.end(agentJson(rangeFrom(req.url), viewFrom(req.url)));
|
|
610
|
+
res.end(agentJson(rangeFrom(req.url), viewFrom(req.url), offsetFrom(req.url)));
|
|
580
611
|
return;
|
|
581
612
|
}
|
|
582
613
|
if (req.url?.startsWith('/api/agent/session')) {
|
|
@@ -691,4 +722,4 @@ async function runDashboard(options) {
|
|
|
691
722
|
});
|
|
692
723
|
});
|
|
693
724
|
}
|
|
694
|
-
export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule };
|
|
725
|
+
export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule, offsetFrom };
|