@krtclcdy/tersio 2.25.4 → 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 +27 -63
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +187 -0
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +29 -13
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +168 -110
- package/dist/cli/doctor-fix.js +232 -50
- package/dist/cli/doctor.js +281 -32
- package/dist/cli/hosts.js +29 -9
- package/dist/cli/install.js +204 -69
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +125 -17
- package/dist/cli/profile.js +14 -9
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +24 -8
- package/dist/cli/update.js +9 -10
- package/dist/cli/usage.js +75 -7
- package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +89 -0
- package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
- package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
- package/dist/extensions/{opencode/server.js → server.js} +98 -14
- package/dist/extensions/shared/agent-stats.js +1198 -0
- package/dist/extensions/shared/brand-marks.js +153 -0
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +12 -7
- package/dist/extensions/shared/pricing.js +86 -30
- 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 +113 -39
- package/dist/extensions/shared/usage-store.js +191 -48
- package/dist/extensions/{lib → shared}/utils.js +6 -6
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +9 -4
- package/extensions/{caveman-session → caveman}/index.ts +4 -2
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +89 -0
- package/extensions/{combo-toggle → combo}/index.ts +62 -19
- package/extensions/{tersio-commands → commands}/index.ts +2 -2
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
- package/extensions/{opencode/server.ts → server.ts} +81 -16
- package/extensions/shared/agent-stats.ts +1605 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +13 -7
- package/extensions/shared/pricing.ts +95 -32
- 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 +134 -46
- package/extensions/shared/usage-store.ts +202 -50
- package/extensions/{lib → shared}/utils.ts +6 -6
- package/extensions/timing/index.ts +45 -0
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
- package/package.json +27 -18
- package/dist/extensions/caveman-session/rule.md +0 -79
- package/extensions/caveman-session/rule.md +0 -79
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';
|
|
@@ -6,17 +6,17 @@ import { promises as fs } from 'node:fs';
|
|
|
6
6
|
import http from 'node:http';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
10
9
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
11
|
-
import { summarizeUsage } from './usage.js';
|
|
12
|
-
import { backupUsageDb, deleteUsageBackup, listUsageBackups, restoreUsageBackup, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
13
10
|
import { isCurrencyCode } from './currency.js';
|
|
14
11
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
15
12
|
import { BACKUP_SCHEDULES, formatCliStatus, storedProfile, writePluginSettings } from './profile.js';
|
|
16
13
|
import { normalizeComboLevel } from '../extensions/shared/session-state.js';
|
|
17
|
-
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS } from './common.js';
|
|
14
|
+
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS } from './common.js';
|
|
18
15
|
import { PACKAGE_NAME } from './common.js';
|
|
19
|
-
import { findPackageRoot, resolveRtkBinary } from '../extensions/
|
|
16
|
+
import { findPackageRoot, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
|
|
17
|
+
import { BACKUP_KEEP, backupDir, backupDiskBytes, createDataBackup, dataSources, deleteDataBackup, listDataBackups, restoreDataBackup } from './backups.js';
|
|
18
|
+
import { readBackupSchedule, syncUsageDb } from '../extensions/shared/usage-store.js';
|
|
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');
|
|
22
22
|
const DASHBOARD_BRAND = path.join(DASHBOARD_DIST, 'brand.webp');
|
|
@@ -49,8 +49,64 @@ async function brandDataUri() {
|
|
|
49
49
|
const brand = await fs.readFile(DASHBOARD_BRAND);
|
|
50
50
|
return `data:image/webp;base64,${brand.toString('base64')}`;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/** The range a page opens on, matching the reference dashboard. */
|
|
53
|
+
const DEFAULT_AGENT_RANGE = '24h';
|
|
54
|
+
/** Poll memo TTL: every poll re-reads the aggregate, so a repeat poll reuses the last answer. */
|
|
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;
|
|
58
|
+
let agentMemo = null;
|
|
59
|
+
let lastSyncAt = 0;
|
|
60
|
+
function fileStamp(file) {
|
|
61
|
+
try {
|
|
62
|
+
const stat = statSync(file);
|
|
63
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return '-';
|
|
67
|
+
}
|
|
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) {
|
|
75
|
+
const held = agentMemo;
|
|
76
|
+
if (held && held.range === range && held.view === view && held.offset === offset && held.stamp === storeStamp() && Date.now() - held.at < AGENT_REFRESH_MS) {
|
|
77
|
+
return held.value;
|
|
78
|
+
}
|
|
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 };
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
/** `?range=` on any of the omp routes; an unknown value falls back to the default. */
|
|
91
|
+
function queryParam(url, name) {
|
|
92
|
+
return (url.split('?')[1] ?? '').split('&').find((part) => part.startsWith(`${name}=`))?.slice(name.length + 1) ?? null;
|
|
93
|
+
}
|
|
94
|
+
function rangeFrom(url) {
|
|
95
|
+
const raw = queryParam(url, 'range');
|
|
96
|
+
return isRangeKey(raw) ? raw : DEFAULT_AGENT_RANGE;
|
|
97
|
+
}
|
|
98
|
+
function viewFrom(url) {
|
|
99
|
+
const raw = queryParam(url, 'view');
|
|
100
|
+
return isAgentView(raw) ? raw : 'all';
|
|
101
|
+
}
|
|
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) });
|
|
54
110
|
}
|
|
55
111
|
// Runs outside any agent session, so it reports the persisted defaults.
|
|
56
112
|
async function statusJson() {
|
|
@@ -61,6 +117,7 @@ async function statusJson() {
|
|
|
61
117
|
caveman: profile.cavemanDefault,
|
|
62
118
|
rtk: profile.rtkDefault ? 'on' : 'off',
|
|
63
119
|
ponytail: profile.ponytailDefault,
|
|
120
|
+
pstack: profile.pstackDefault,
|
|
64
121
|
});
|
|
65
122
|
}
|
|
66
123
|
// Local-only health: find each agent on PATH, run `--version`, report both.
|
|
@@ -245,18 +302,29 @@ function computeDoctorRows() {
|
|
|
245
302
|
}
|
|
246
303
|
catch { /* missing config is reported by extension rows */ }
|
|
247
304
|
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
248
|
-
addon('Unique config registrations',
|
|
249
|
-
// 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.
|
|
250
307
|
const dangling = explicitEntries
|
|
251
308
|
.filter((entry) => entry.includes(`extensions${path.sep}`) || entry.includes('extensions/'))
|
|
252
309
|
.filter((entry) => !ok(path.isAbsolute(entry) ? entry : path.resolve(OMP_AGENT_DIR, entry)));
|
|
253
310
|
addon('Extension files present', dangling.length === 0, dangling.length ? dangling.join(', ') : 'ok', 'Extensions');
|
|
254
|
-
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman
|
|
255
|
-
ext('
|
|
256
|
-
|
|
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');
|
|
257
315
|
const ruleAge = ageStr(rule);
|
|
258
316
|
const ruleAt = absTime(rule);
|
|
259
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})`);
|
|
260
328
|
const rtkVer = rtkBin ? rtkVersion(rtkBin) : null;
|
|
261
329
|
const rtkAge = rtkBin ? ageStr(rtkBin) : null;
|
|
262
330
|
const rtkAt = rtkBin ? absTime(rtkBin) : null;
|
|
@@ -277,7 +345,7 @@ function computeDoctorRows() {
|
|
|
277
345
|
addon('Prices feed', pricesAge !== null, pricesAge && pricesAt ? `(pulled ${pricesAge} · ${pricesAt})` : prices, 'Usage');
|
|
278
346
|
return rows;
|
|
279
347
|
}
|
|
280
|
-
//
|
|
348
|
+
// A report holding any of these labels is stale by definition.
|
|
281
349
|
const RETIRED_DIAG_LABELS = new Set([
|
|
282
350
|
'OMP CLI',
|
|
283
351
|
'Tersio CLI',
|
|
@@ -298,11 +366,10 @@ const RETIRED_DIAG_LABELS = new Set([
|
|
|
298
366
|
function isRetiredReport(report) {
|
|
299
367
|
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
300
368
|
}
|
|
301
|
-
// Recompute when forced, missing, retired, or past
|
|
369
|
+
// Recompute when forced, missing, retired, or past schedule; manual schedules recompute on open, dated ones reuse.
|
|
302
370
|
function getDoctorReport(force) {
|
|
303
371
|
const saved = readDiagReport();
|
|
304
372
|
const ttl = saved ? DIAG_TTL_MS[saved.schedule] : 0;
|
|
305
|
-
// Manual recomputes on open; a dated schedule reuses the saved report.
|
|
306
373
|
if (!force && saved && !isRetiredReport(saved) && ttl > 0 && Date.now() - saved.checkedAt < ttl)
|
|
307
374
|
return saved;
|
|
308
375
|
const report = { rows: computeDoctorRows(), checkedAt: Date.now(), schedule: saved?.schedule ?? 'manual' };
|
|
@@ -336,6 +403,12 @@ async function saveDefaults(body) {
|
|
|
336
403
|
return { ok: false, error: 'unknown ponytail level' };
|
|
337
404
|
profile.ponytailDefault = v;
|
|
338
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
|
+
}
|
|
339
412
|
if (body.backupSchedule !== undefined) {
|
|
340
413
|
const v = String(body.backupSchedule);
|
|
341
414
|
if (!BACKUP_SCHEDULES.has(v))
|
|
@@ -355,6 +428,7 @@ async function saveDefaults(body) {
|
|
|
355
428
|
cavemanDefault: profile.cavemanDefault,
|
|
356
429
|
rtkDefault: profile.rtkDefault,
|
|
357
430
|
ponytailDefault: profile.ponytailDefault,
|
|
431
|
+
pstackDefault: profile.pstackDefault,
|
|
358
432
|
backupSchedule: profile.backupSchedule,
|
|
359
433
|
},
|
|
360
434
|
};
|
|
@@ -374,50 +448,6 @@ async function saveDashboardCurrency(raw) {
|
|
|
374
448
|
await writePluginSettings(profile, {});
|
|
375
449
|
return code;
|
|
376
450
|
}
|
|
377
|
-
export const EXPORT_FORMATS = ['json', 'jsonl', 'csv'];
|
|
378
|
-
export function exportRows(report) {
|
|
379
|
-
return report.recent.map((r) => ({
|
|
380
|
-
timestamp: new Date(r.t).toISOString(),
|
|
381
|
-
local: new Date(r.t).toLocaleString(),
|
|
382
|
-
agent: r.h ?? 'pi',
|
|
383
|
-
model: r.m,
|
|
384
|
-
input: r.i,
|
|
385
|
-
output: r.o,
|
|
386
|
-
cacheRead: r.cr ?? 0,
|
|
387
|
-
cacheWrite: r.cw ?? 0,
|
|
388
|
-
costUsd: r.usd,
|
|
389
|
-
elapsedMs: r.d,
|
|
390
|
-
status: r.st,
|
|
391
|
-
}));
|
|
392
|
-
}
|
|
393
|
-
export function csvCell(v) {
|
|
394
|
-
const s = v === undefined || v === null ? '' : String(v);
|
|
395
|
-
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
396
|
-
}
|
|
397
|
-
export function exportBody(format, report) {
|
|
398
|
-
const stamp = new Date().toISOString().slice(0, 10);
|
|
399
|
-
if (format === 'json') {
|
|
400
|
-
return { body: JSON.stringify({ exportedAt: new Date().toISOString(), version: report.version, source: report.source, report }, null, 2), type: 'application/json' };
|
|
401
|
-
}
|
|
402
|
-
const rows = exportRows(report);
|
|
403
|
-
if (format === 'jsonl') {
|
|
404
|
-
return { body: rows.map((r) => JSON.stringify(r)).join('\n') + '\n', type: 'application/x-ndjson' };
|
|
405
|
-
}
|
|
406
|
-
const cols = Object.keys(rows[0] ?? { timestamp: '', agent: '' });
|
|
407
|
-
const head = cols.join(',');
|
|
408
|
-
const body = [head, ...rows.map((r) => cols.map((c) => csvCell(r[c])).join(','))].join('\n') + '\n';
|
|
409
|
-
return { body, type: 'text/csv; charset=utf-8' };
|
|
410
|
-
}
|
|
411
|
-
function serveExport(req, res) {
|
|
412
|
-
const format = ((req.url || '').split('?')[1] || '').match(/format=([a-z]+)/)?.[1];
|
|
413
|
-
const fmt = format && EXPORT_FORMATS.includes(format) ? format : 'json';
|
|
414
|
-
const { body, type } = exportBody(fmt, summarizeUsage(readUsage()));
|
|
415
|
-
res.writeHead(200, {
|
|
416
|
-
'Content-Type': type,
|
|
417
|
-
'Content-Disposition': `attachment; filename="tersio-usage-${new Date().toISOString().slice(0, 10)}.${fmt === 'jsonl' ? 'ndjson' : fmt}"`,
|
|
418
|
-
});
|
|
419
|
-
res.end(body);
|
|
420
|
-
}
|
|
421
451
|
function readBody(req) {
|
|
422
452
|
return new Promise((resolve) => {
|
|
423
453
|
let body = '';
|
|
@@ -430,7 +460,7 @@ function openBrowser(url) {
|
|
|
430
460
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
431
461
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
432
462
|
}
|
|
433
|
-
// Replacers throughout: session data holds `$'` (shell quoting) and
|
|
463
|
+
// Replacers throughout: session data holds `$'` (shell quoting) and `</script>`, which String.replace would mangle.
|
|
434
464
|
function escapeInline(json) {
|
|
435
465
|
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
436
466
|
}
|
|
@@ -448,73 +478,78 @@ function parseOwnJson(json, field) {
|
|
|
448
478
|
}
|
|
449
479
|
return parsed;
|
|
450
480
|
}
|
|
451
|
-
async function exportDashboard(exportFile) {
|
|
481
|
+
async function exportDashboard(exportFile, currency) {
|
|
452
482
|
requireDashboardBundle();
|
|
453
483
|
const [bundle, icon] = await Promise.all([readSegment(DASHBOARD_INDEX), brandDataUri()]);
|
|
484
|
+
// Minified: fetch reads `fetch(\`brand.webp\`)` or `fetch("brand.webp")`; a mismatch loses the card's mark.
|
|
485
|
+
const b64 = icon.split(',')[1] ?? '';
|
|
486
|
+
// atob yields one byte per char, so a Uint8Array is needed or bytes >= 0x80 double and corrupt the webp.
|
|
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; })()`;
|
|
454
488
|
const inline = bundle
|
|
455
|
-
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({
|
|
456
|
-
|
|
457
|
-
.replace(/src
|
|
458
|
-
.replace(
|
|
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) }))};`)
|
|
490
|
+
// A mark is an HTML attribute or JSX prop; match every quote or the file keeps an unresolvable reference.
|
|
491
|
+
.replace(/((?:src|href)[=:][ \t]*)([`"'])brand\.webp\2/g, (_all, head, quote) => `${head}${quote}${icon}${quote}`)
|
|
492
|
+
.replace(/fetch\(([`"])brand\.webp\1\)/g, () => `Promise.resolve({ ok: true, blob: async () => new Blob([${brandBytes}], { type: "image/webp" }) })`);
|
|
459
493
|
await fs.writeFile(exportFile, inline, 'utf8');
|
|
460
494
|
console.log(`[ok] Dashboard exported → ${exportFile}`);
|
|
461
495
|
}
|
|
462
496
|
async function runDashboard(options) {
|
|
463
497
|
if (options.exportFile) {
|
|
464
|
-
await exportDashboard(options.exportFile);
|
|
498
|
+
await exportDashboard(options.exportFile, options.currency);
|
|
465
499
|
return;
|
|
466
500
|
}
|
|
467
501
|
requireDashboardBundle();
|
|
468
502
|
const server = http.createServer(async (req, res) => {
|
|
469
|
-
if (req.url === '/
|
|
470
|
-
const rows = clearUsageLedger();
|
|
471
|
-
markReset();
|
|
472
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
473
|
-
res.end(JSON.stringify({ ok: true, rows }));
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (req.url === '/backups' && req.method === 'GET') {
|
|
477
|
-
const rows = listUsageBackups().map((b) => ({ file: path.basename(b.file), mtime: b.mtime, size: b.size }));
|
|
503
|
+
if (req.url === '/api/data' && req.method === 'GET') {
|
|
478
504
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
479
|
-
res.end(JSON.stringify({
|
|
505
|
+
res.end(JSON.stringify({
|
|
506
|
+
dir: backupDir(),
|
|
507
|
+
keep: BACKUP_KEEP,
|
|
508
|
+
sources: dataSources(),
|
|
509
|
+
backups: listDataBackups(),
|
|
510
|
+
schedule: readBackupSchedule(),
|
|
511
|
+
bytes: backupDiskBytes(),
|
|
512
|
+
}));
|
|
480
513
|
return;
|
|
481
514
|
}
|
|
482
|
-
if (req.url === '/
|
|
483
|
-
|
|
484
|
-
backupUsageDb(usageDbPath());
|
|
485
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
486
|
-
res.end(JSON.stringify({ ok: true }));
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
489
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
490
|
-
res.end(JSON.stringify({ ok: false, error: 'backup failed' }));
|
|
491
|
-
}
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
if (req.url === '/backups/delete' && req.method === 'POST') {
|
|
495
|
-
let file = '';
|
|
515
|
+
if (req.url === '/api/data' && req.method === 'POST') {
|
|
516
|
+
let body = {};
|
|
496
517
|
try {
|
|
497
|
-
|
|
518
|
+
body = JSON.parse(await readBody(req)) ?? {};
|
|
498
519
|
}
|
|
499
520
|
catch {
|
|
500
|
-
|
|
521
|
+
body = {};
|
|
501
522
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
if (req.url === '/backups/restore' && req.method === 'POST') {
|
|
508
|
-
let file = '';
|
|
523
|
+
const action = String(body.action ?? '');
|
|
524
|
+
const name = String(body.name ?? '');
|
|
525
|
+
// A missing SQLite engine, or a locked file, is a 500 with a message rather than a silent no-op.
|
|
509
526
|
try {
|
|
510
|
-
|
|
527
|
+
if (action === 'create') {
|
|
528
|
+
const made = await createDataBackup();
|
|
529
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
530
|
+
res.end(JSON.stringify({ ok: true, name: made.name, files: made.files.length }));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (action === 'restore') {
|
|
534
|
+
const ok = await restoreDataBackup(name);
|
|
535
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
536
|
+
res.end(JSON.stringify(ok ? { ok: true, name } : { ok: false, error: 'unknown snapshot' }));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (action === 'delete') {
|
|
540
|
+
const ok = await deleteDataBackup(name);
|
|
541
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
542
|
+
res.end(JSON.stringify(ok ? { ok: true, name } : { ok: false, error: 'unknown snapshot' }));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
511
545
|
}
|
|
512
|
-
catch {
|
|
513
|
-
|
|
546
|
+
catch (error) {
|
|
547
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
548
|
+
res.end(JSON.stringify({ ok: false, error: error.message || 'backup failed' }));
|
|
549
|
+
return;
|
|
514
550
|
}
|
|
515
|
-
|
|
516
|
-
res.
|
|
517
|
-
res.end(JSON.stringify(ok ? { ok: true, file } : { ok: false, error: 'unknown backup' }));
|
|
551
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
552
|
+
res.end(JSON.stringify({ ok: false, error: 'unknown action' }));
|
|
518
553
|
return;
|
|
519
554
|
}
|
|
520
555
|
if (req.url === '/settings' && req.method === 'GET') {
|
|
@@ -525,7 +560,9 @@ async function runDashboard(options) {
|
|
|
525
560
|
cavemanDefault: profile.cavemanDefault,
|
|
526
561
|
rtkDefault: profile.rtkDefault,
|
|
527
562
|
ponytailDefault: profile.ponytailDefault,
|
|
563
|
+
pstackDefault: profile.pstackDefault,
|
|
528
564
|
backupSchedule: profile.backupSchedule,
|
|
565
|
+
currency: options.currency,
|
|
529
566
|
}));
|
|
530
567
|
});
|
|
531
568
|
return;
|
|
@@ -568,13 +605,34 @@ async function runDashboard(options) {
|
|
|
568
605
|
}
|
|
569
606
|
return;
|
|
570
607
|
}
|
|
571
|
-
if (req.url === '/
|
|
608
|
+
if (req.url === '/api/agent' || req.url?.startsWith('/api/agent?')) {
|
|
609
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
610
|
+
res.end(agentJson(rangeFrom(req.url), viewFrom(req.url), offsetFrom(req.url)));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (req.url?.startsWith('/api/agent/session')) {
|
|
614
|
+
const file = decodeURIComponent(queryParam(req.url, 'file') ?? '');
|
|
615
|
+
const body = file ? readSessionTrace(file) : {
|
|
616
|
+
sessionFile: '', project: '', title: '', cwd: '', startedAt: 0, endedAt: 0,
|
|
617
|
+
tracks: [], markers: [], summary: {
|
|
618
|
+
wallMs: 0, modelMs: 0, toolMs: 0, idleMs: 0, turns: 0, requests: 0, toolCalls: 0, agents: 0,
|
|
619
|
+
totalTokens: 0, costUsd: 0,
|
|
620
|
+
},
|
|
621
|
+
toolStats: [], truncated: false,
|
|
622
|
+
};
|
|
623
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
624
|
+
res.end(JSON.stringify(body));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (req.url?.startsWith('/api/agent/entry')) {
|
|
628
|
+
const file = decodeURIComponent(queryParam(req.url, 'file') ?? '');
|
|
629
|
+
const entry = decodeURIComponent(queryParam(req.url, 'entry') ?? '');
|
|
630
|
+
const body = file && entry ? readAgentRequestEntry(file, entry) : emptyRequestEntry();
|
|
572
631
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
573
|
-
res.end(
|
|
632
|
+
res.end(JSON.stringify(body));
|
|
574
633
|
return;
|
|
575
634
|
}
|
|
576
635
|
if (req.url?.startsWith('/export')) {
|
|
577
|
-
serveExport(req, res);
|
|
578
636
|
return;
|
|
579
637
|
}
|
|
580
638
|
if (req.url === '/health') {
|
|
@@ -664,4 +722,4 @@ async function runDashboard(options) {
|
|
|
664
722
|
});
|
|
665
723
|
});
|
|
666
724
|
}
|
|
667
|
-
export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule };
|
|
725
|
+
export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule, offsetFrom };
|