@krtclcdy/tersio 2.24.0 → 2.24.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 +1 -3
- package/cli/banner.js +2 -3
- package/cli/currency.js +3 -7
- package/cli/dashboard.js +167 -3
- package/cli/doctor-fix.js +15 -12
- package/cli/doctor.js +1 -12
- package/cli/hosts.js +7 -11
- package/cli/install.js +113 -71
- package/cli/interactive.js +36 -2
- package/cli/layer.js +1 -3
- package/cli/manifest.js +1 -4
- package/cli/profile.js +9 -7
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +1 -6
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +43 -24
- package/cli/update.js +7 -8
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +6 -5
- package/extensions/ai-addons-updater/index.ts +7 -6
- package/extensions/caveman-session/index.js +14 -29
- package/extensions/caveman-session/index.ts +15 -30
- package/extensions/combo-toggle/index.js +10 -14
- package/extensions/combo-toggle/index.ts +10 -15
- package/extensions/lib/utils.js +4 -0
- package/extensions/lib/utils.ts +3 -0
- package/extensions/rtk-session/index.js +27 -31
- package/extensions/rtk-session/index.ts +25 -29
- package/extensions/shared/carbon.js +3 -8
- package/extensions/shared/carbon.ts +4 -8
- package/extensions/shared/host.js +1 -11
- package/extensions/shared/host.ts +1 -11
- package/extensions/shared/plugin-settings.js +4 -14
- package/extensions/shared/plugin-settings.ts +4 -14
- package/extensions/shared/pricing.js +4 -6
- package/extensions/shared/pricing.ts +4 -6
- package/extensions/shared/rtk-gain.js +2 -7
- package/extensions/shared/rtk-gain.ts +2 -7
- package/extensions/shared/session-state.js +30 -18
- package/extensions/shared/session-state.ts +33 -16
- package/extensions/shared/usage-ledger.js +138 -17
- package/extensions/shared/usage-ledger.ts +143 -19
- package/extensions/shared/usage-store.js +175 -23
- package/extensions/shared/usage-store.ts +171 -22
- package/extensions/tersio-commands/index.js +1 -5
- package/extensions/tersio-commands/index.ts +1 -5
- package/package.json +6 -7
- package/tersio.js +1 -3
package/README.md
CHANGED
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
[](https://github.com/KurutoDenzeru/tersio/actions)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
Token
|
|
12
|
-
RTK shell output, lean Ponytail code calls, usage tracking with a gain
|
|
13
|
-
dashboard — one-command combo presets, one shared extension tree.
|
|
11
|
+
Token saver & usage tracker for coding agents: terse caveman replies, compact RTK shell output, lean Ponytail code calls, and a gain dashboard — one-command combo presets over one shared extension tree.
|
|
14
12
|
|
|
15
13
|
## ⚡ Getting Started
|
|
16
14
|
|
package/cli/banner.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// cli/banner.ts — mini pixel-scissor welcome
|
|
2
|
-
//
|
|
3
|
-
// TTY-only and skipped on --dry-run, so piped output stays byte-identical.
|
|
1
|
+
// cli/banner.ts — mini pixel-scissor welcome. TTY-only and skipped on
|
|
2
|
+
// --dry-run, so piped output stays byte-identical.
|
|
4
3
|
import { intro as clackIntro, note as clackNote } from '@clack/prompts';
|
|
5
4
|
import { PACKAGE_VERSION, dryRun } from './common.js';
|
|
6
5
|
function tty() {
|
package/cli/currency.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// cli/currency.ts —
|
|
2
|
-
//
|
|
3
|
-
// converter (dashboard/app); the dashboard replaces rates with live
|
|
4
|
-
// frankfurter figures when reachable, the CLI always uses the snapshot.
|
|
1
|
+
// cli/currency.ts — USD-base currency helpers on offline snapshot rates. Same
|
|
2
|
+
// 10 currencies as the dashboard, which prefers live frankfurter figures.
|
|
5
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
4
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
7
5
|
const CURRENCY_CODES = [
|
|
@@ -31,9 +29,7 @@ function parseCurrencyFlag(raw) {
|
|
|
31
29
|
}
|
|
32
30
|
return v;
|
|
33
31
|
}
|
|
34
|
-
// Stored default
|
|
35
|
-
// USD when missing, corrupt, or holding an unknown code. Sync so flag
|
|
36
|
-
// parsing in cli/common.ts can fall back to it at startup.
|
|
32
|
+
// Stored default; USD when missing, corrupt, or an unknown code.
|
|
37
33
|
function readStoredCurrency() {
|
|
38
34
|
const file = tersioSettingsFile();
|
|
39
35
|
if (!existsSync(file))
|
package/cli/dashboard.js
CHANGED
|
@@ -10,9 +10,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
11
11
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
12
12
|
import { summarizeUsage } from './usage.js';
|
|
13
|
+
import { deleteUsageBackup, listUsageBackups, restoreUsageBackup } from '../extensions/shared/usage-store.js';
|
|
13
14
|
import { isCurrencyCode } from './currency.js';
|
|
14
15
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
15
|
-
import { storedProfile, writePluginSettings } from './profile.js';
|
|
16
|
+
import { BACKUP_SCHEDULES, storedProfile, writePluginSettings } from './profile.js';
|
|
17
|
+
import { normalizeComboLevel } from '../extensions/shared/session-state.js';
|
|
18
|
+
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS } from './common.js';
|
|
16
19
|
import { PACKAGE_NAME } from './common.js';
|
|
17
20
|
import { resolveRtkBinary } from '../extensions/lib/utils.js';
|
|
18
21
|
const DASHBOARD_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard', 'dist');
|
|
@@ -217,8 +220,12 @@ function computeDoctorRows() {
|
|
|
217
220
|
catch { /* missing config is reported by extension rows */ }
|
|
218
221
|
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
219
222
|
addon('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : 'ok', 'Extensions');
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
// A listed extension whose file is gone makes the host warn on every load, so
|
|
224
|
+
// surface it rather than leaving the user with only a startup message.
|
|
225
|
+
const dangling = explicitEntries
|
|
226
|
+
.filter((entry) => entry.includes(`extensions${path.sep}`) || entry.includes('extensions/'))
|
|
227
|
+
.filter((entry) => !ok(path.isAbsolute(entry) ? entry : path.resolve(OMP_AGENT_DIR, entry)));
|
|
228
|
+
addon('Extension files present', dangling.length === 0, dangling.length ? dangling.join(', ') : 'ok', 'Extensions');
|
|
222
229
|
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
|
|
223
230
|
ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
|
|
224
231
|
ext('Ponytail extension', path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
|
|
@@ -258,6 +265,7 @@ const RETIRED_DIAG_LABELS = new Set([
|
|
|
258
265
|
'Self plugin',
|
|
259
266
|
'Ponytail registered',
|
|
260
267
|
'Usage store',
|
|
268
|
+
'RTK OMP wiring (rtk.ts)',
|
|
261
269
|
]);
|
|
262
270
|
function isRetiredReport(report) {
|
|
263
271
|
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
@@ -279,6 +287,50 @@ function setDiagSchedule(schedule) {
|
|
|
279
287
|
writeDiagReport(base);
|
|
280
288
|
return base;
|
|
281
289
|
}
|
|
290
|
+
// One field per call, validated against the same sets the CLI uses.
|
|
291
|
+
async function saveDefaults(body) {
|
|
292
|
+
const profile = await storedProfile();
|
|
293
|
+
if (body.comboDefault !== undefined) {
|
|
294
|
+
const level = normalizeComboLevel(body.comboDefault);
|
|
295
|
+
if (!level)
|
|
296
|
+
return { ok: false, error: 'unknown combo level' };
|
|
297
|
+
profile.comboDefault = level;
|
|
298
|
+
}
|
|
299
|
+
if (body.cavemanDefault !== undefined) {
|
|
300
|
+
const v = String(body.cavemanDefault);
|
|
301
|
+
if (!CAVEMAN_DEFAULTS.has(v))
|
|
302
|
+
return { ok: false, error: 'unknown caveman level' };
|
|
303
|
+
profile.cavemanDefault = v;
|
|
304
|
+
}
|
|
305
|
+
if (body.ponytailDefault !== undefined) {
|
|
306
|
+
const v = String(body.ponytailDefault);
|
|
307
|
+
if (!PONYTAIL_DEFAULTS.has(v))
|
|
308
|
+
return { ok: false, error: 'unknown ponytail level' };
|
|
309
|
+
profile.ponytailDefault = v;
|
|
310
|
+
}
|
|
311
|
+
if (body.backupSchedule !== undefined) {
|
|
312
|
+
const v = String(body.backupSchedule);
|
|
313
|
+
if (!BACKUP_SCHEDULES.has(v))
|
|
314
|
+
return { ok: false, error: 'unknown backup schedule' };
|
|
315
|
+
profile.backupSchedule = v;
|
|
316
|
+
}
|
|
317
|
+
if (body.rtkDefault !== undefined) {
|
|
318
|
+
if (typeof body.rtkDefault !== 'boolean')
|
|
319
|
+
return { ok: false, error: 'rtk default must be true or false' };
|
|
320
|
+
profile.rtkDefault = body.rtkDefault;
|
|
321
|
+
}
|
|
322
|
+
await writePluginSettings(profile, {});
|
|
323
|
+
return {
|
|
324
|
+
ok: true,
|
|
325
|
+
profile: {
|
|
326
|
+
comboDefault: profile.comboDefault,
|
|
327
|
+
cavemanDefault: profile.cavemanDefault,
|
|
328
|
+
rtkDefault: profile.rtkDefault,
|
|
329
|
+
ponytailDefault: profile.ponytailDefault,
|
|
330
|
+
backupSchedule: profile.backupSchedule,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
282
334
|
function readDiagSchedule() {
|
|
283
335
|
return readDiagReport()?.schedule ?? 'manual';
|
|
284
336
|
}
|
|
@@ -295,6 +347,50 @@ async function saveDashboardCurrency(raw) {
|
|
|
295
347
|
await writePluginSettings(profile, {});
|
|
296
348
|
return code;
|
|
297
349
|
}
|
|
350
|
+
export const EXPORT_FORMATS = ['json', 'jsonl', 'csv'];
|
|
351
|
+
export function exportRows(report) {
|
|
352
|
+
return report.recent.map((r) => ({
|
|
353
|
+
timestamp: new Date(r.t).toISOString(),
|
|
354
|
+
local: new Date(r.t).toLocaleString(),
|
|
355
|
+
agent: r.h ?? 'pi',
|
|
356
|
+
model: r.m,
|
|
357
|
+
input: r.i,
|
|
358
|
+
output: r.o,
|
|
359
|
+
cacheRead: r.cr ?? 0,
|
|
360
|
+
cacheWrite: r.cw ?? 0,
|
|
361
|
+
costUsd: r.usd,
|
|
362
|
+
elapsedMs: r.d,
|
|
363
|
+
status: r.st,
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
export function csvCell(v) {
|
|
367
|
+
const s = v === undefined || v === null ? '' : String(v);
|
|
368
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
369
|
+
}
|
|
370
|
+
export function exportBody(format, report) {
|
|
371
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
372
|
+
if (format === 'json') {
|
|
373
|
+
return { body: JSON.stringify({ exportedAt: new Date().toISOString(), version: report.version, source: report.source, report }, null, 2), type: 'application/json' };
|
|
374
|
+
}
|
|
375
|
+
const rows = exportRows(report);
|
|
376
|
+
if (format === 'jsonl') {
|
|
377
|
+
return { body: rows.map((r) => JSON.stringify(r)).join('\n') + '\n', type: 'application/x-ndjson' };
|
|
378
|
+
}
|
|
379
|
+
const cols = Object.keys(rows[0] ?? { timestamp: '', agent: '' });
|
|
380
|
+
const head = cols.join(',');
|
|
381
|
+
const body = [head, ...rows.map((r) => cols.map((c) => csvCell(r[c])).join(','))].join('\n') + '\n';
|
|
382
|
+
return { body, type: 'text/csv; charset=utf-8' };
|
|
383
|
+
}
|
|
384
|
+
function serveExport(req, res) {
|
|
385
|
+
const format = ((req.url || '').split('?')[1] || '').match(/format=([a-z]+)/)?.[1];
|
|
386
|
+
const fmt = format && EXPORT_FORMATS.includes(format) ? format : 'json';
|
|
387
|
+
const { body, type } = exportBody(fmt, summarizeUsage(readUsage()));
|
|
388
|
+
res.writeHead(200, {
|
|
389
|
+
'Content-Type': type,
|
|
390
|
+
'Content-Disposition': `attachment; filename="tersio-usage-${new Date().toISOString().slice(0, 10)}.${fmt === 'jsonl' ? 'ndjson' : fmt}"`,
|
|
391
|
+
});
|
|
392
|
+
res.end(body);
|
|
393
|
+
}
|
|
298
394
|
function readBody(req) {
|
|
299
395
|
return new Promise((resolve) => {
|
|
300
396
|
let body = '';
|
|
@@ -351,6 +447,70 @@ async function runDashboard(options) {
|
|
|
351
447
|
res.end(JSON.stringify({ ok: true, rows }));
|
|
352
448
|
return;
|
|
353
449
|
}
|
|
450
|
+
if (req.url === '/backups' && req.method === 'GET') {
|
|
451
|
+
const rows = listUsageBackups().map((b) => ({ file: path.basename(b.file), mtime: b.mtime, size: b.size }));
|
|
452
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
453
|
+
res.end(JSON.stringify({ backups: rows }));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (req.url === '/backups/delete' && req.method === 'POST') {
|
|
457
|
+
let file = '';
|
|
458
|
+
try {
|
|
459
|
+
file = String(JSON.parse(await readBody(req)).file ?? '');
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
file = '';
|
|
463
|
+
}
|
|
464
|
+
const ok = deleteUsageBackup(file);
|
|
465
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
466
|
+
res.end(JSON.stringify(ok ? { ok: true, file } : { ok: false, error: 'unknown backup' }));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (req.url === '/backups/restore' && req.method === 'POST') {
|
|
470
|
+
let file = '';
|
|
471
|
+
try {
|
|
472
|
+
file = String(JSON.parse(await readBody(req)).file ?? '');
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
file = '';
|
|
476
|
+
}
|
|
477
|
+
const ok = restoreUsageBackup(file);
|
|
478
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
479
|
+
res.end(JSON.stringify(ok ? { ok: true, file } : { ok: false, error: 'unknown backup' }));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (req.url === '/settings' && req.method === 'GET') {
|
|
483
|
+
storedProfile().then((profile) => {
|
|
484
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
485
|
+
res.end(JSON.stringify({
|
|
486
|
+
comboDefault: profile.comboDefault,
|
|
487
|
+
cavemanDefault: profile.cavemanDefault,
|
|
488
|
+
rtkDefault: profile.rtkDefault,
|
|
489
|
+
ponytailDefault: profile.ponytailDefault,
|
|
490
|
+
backupSchedule: profile.backupSchedule,
|
|
491
|
+
}));
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (req.url === '/settings' && req.method === 'POST') {
|
|
496
|
+
let body = {};
|
|
497
|
+
try {
|
|
498
|
+
body = JSON.parse(await readBody(req)) ?? {};
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
body = {};
|
|
502
|
+
}
|
|
503
|
+
const saved = await saveDefaults(body);
|
|
504
|
+
if (!saved.ok) {
|
|
505
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
506
|
+
res.end(JSON.stringify({ ok: false, error: saved.error }));
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
510
|
+
res.end(JSON.stringify({ ok: true, ...saved.profile }));
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
354
514
|
if (req.url === '/currency' && req.method === 'POST') {
|
|
355
515
|
let code = null;
|
|
356
516
|
try {
|
|
@@ -375,6 +535,10 @@ async function runDashboard(options) {
|
|
|
375
535
|
res.end(dataJson());
|
|
376
536
|
return;
|
|
377
537
|
}
|
|
538
|
+
if (req.url?.startsWith('/export')) {
|
|
539
|
+
serveExport(req, res);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
378
542
|
if (req.url === '/health') {
|
|
379
543
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
380
544
|
res.end(healthJson());
|
package/cli/doctor-fix.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, execP, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
6
|
-
import { execNetwork } from './interactive.js';
|
|
6
|
+
import { execNetwork, sayTagged } 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';
|
|
9
9
|
import { runLatestUpdate } from './update.js';
|
|
@@ -25,12 +25,12 @@ async function fixExtensionTrees() {
|
|
|
25
25
|
const from = path.join(EXT_DIR, ...file.split('/'));
|
|
26
26
|
const text = await readTextIfExists(from);
|
|
27
27
|
if (text === null)
|
|
28
|
-
|
|
28
|
+
sayTagged(` [warn] bundled source missing: ${from}`);
|
|
29
29
|
else
|
|
30
30
|
await writeIfChanged(path.join(dest, file), text, { dryRun, verbose });
|
|
31
31
|
}
|
|
32
32
|
if (!dryRun)
|
|
33
|
-
|
|
33
|
+
sayTagged(` [ok] ${host.label} extension tree: ${dest}`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
async function fixExtensions(pluginsDir) {
|
|
@@ -39,13 +39,16 @@ async function fixExtensions(pluginsDir) {
|
|
|
39
39
|
const tersioPluginDir = path.join(pluginsDir, 'node_modules', '@krtclcdy', 'tersio');
|
|
40
40
|
const pluginExtDir = path.join(tersioPluginDir, 'extensions');
|
|
41
41
|
await fs.mkdir(pluginExtDir, { recursive: true });
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// rule.md is skipped: its live content is the upstream fetch, not the bundle.
|
|
43
|
+
const bundledTree = TREE_FILES.filter((file) => file !== 'caveman-session/rule.md');
|
|
44
|
+
for (const file of bundledTree) {
|
|
45
|
+
const text = await readTextIfExists(sourcePath(file));
|
|
44
46
|
if (text === null)
|
|
45
|
-
|
|
47
|
+
sayTagged(` [warn] bundled source missing: ${sourcePath(file)}`);
|
|
46
48
|
else
|
|
47
|
-
await writeIfChanged(path.join(pluginExtDir,
|
|
49
|
+
await writeIfChanged(path.join(pluginExtDir, file), text, { dryRun, verbose });
|
|
48
50
|
}
|
|
51
|
+
// Restore only a missing rule; a stale one is /ai-addons' to report.
|
|
49
52
|
const ruleDest = path.join(pluginExtDir, 'caveman-session', 'rule.md');
|
|
50
53
|
if ((await readTextIfExists(ruleDest)) === null) {
|
|
51
54
|
const bundled = await readTextIfExists(path.join(EXT_DIR, 'caveman-session', 'rule.md'));
|
|
@@ -69,7 +72,7 @@ async function fixRegistrations(agentDir, pluginsDir) {
|
|
|
69
72
|
const configPath = path.join(agentDir, 'config.yml');
|
|
70
73
|
if ((await readTextIfExists(configPath)) === null) {
|
|
71
74
|
if (dryRun)
|
|
72
|
-
|
|
75
|
+
sayTagged(` [dry-run] would create ${configPath}`);
|
|
73
76
|
else {
|
|
74
77
|
await fs.mkdir(agentDir, { recursive: true });
|
|
75
78
|
await fs.writeFile(configPath, 'extensions:\n', 'utf8');
|
|
@@ -88,7 +91,7 @@ async function fixRegistrations(agentDir, pluginsDir) {
|
|
|
88
91
|
if (!(PACKAGE_NAME in pkg.dependencies)) {
|
|
89
92
|
pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
|
|
90
93
|
if (dryRun)
|
|
91
|
-
|
|
94
|
+
sayTagged(` [dry-run] would register ${PACKAGE_NAME} in ${pkgPath}`);
|
|
92
95
|
else {
|
|
93
96
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
94
97
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
@@ -99,7 +102,7 @@ async function fixRtk(binDir) {
|
|
|
99
102
|
console.log(' Doctor --fix: repairing RTK binary + wiring');
|
|
100
103
|
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
101
104
|
if (dryRun) {
|
|
102
|
-
|
|
105
|
+
sayTagged(` [dry-run] would download a checksum-verified rtk to ${binDest} and wire it into OMP`);
|
|
103
106
|
return;
|
|
104
107
|
}
|
|
105
108
|
const spec = rtkPlatformSpec();
|
|
@@ -180,11 +183,11 @@ async function runDoctorRepairs(targets) {
|
|
|
180
183
|
try {
|
|
181
184
|
await work();
|
|
182
185
|
if (!dryRun)
|
|
183
|
-
|
|
186
|
+
sayTagged(` [ok] ${label}`);
|
|
184
187
|
}
|
|
185
188
|
catch (e) {
|
|
186
189
|
failed.push(label);
|
|
187
|
-
|
|
190
|
+
sayTagged(` [fail] ${label}: ${(e.message || '').slice(0, 200)}`);
|
|
188
191
|
}
|
|
189
192
|
};
|
|
190
193
|
if (want('extensions'))
|
package/cli/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
|
-
import { detectHosts,
|
|
5
|
+
import { detectHosts, ompPackageDir } from './hosts.js';
|
|
6
6
|
import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
|
|
7
7
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
8
8
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
@@ -115,22 +115,11 @@ async function runDoctor(recheck = false) {
|
|
|
115
115
|
.filter((line) => line.startsWith('/') || line.startsWith('.'));
|
|
116
116
|
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
117
117
|
check('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : '');
|
|
118
|
-
const retiredReinforcement = explicitEntries.filter((entry) => entry.endsWith('/shared/mode-reinforcement.ts')).length;
|
|
119
|
-
if (retiredReinforcement === 0)
|
|
120
|
-
check('No retired reinforcement', true);
|
|
121
|
-
else
|
|
122
|
-
warnLine('Retired reinforcement registration', `${retiredReinforcement} found; run doctor --fix registrations`);
|
|
123
118
|
check('Caveman extension', existsSync(path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
|
|
124
119
|
check('RTK extension', existsSync(path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
|
|
125
120
|
check('Updater extension', existsSync(path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts'));
|
|
126
121
|
check('Ponytail extension', existsSync(path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js')), existsSync(path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js')) ? '' : path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
|
|
127
122
|
}
|
|
128
|
-
for (const host of hosts) {
|
|
129
|
-
if (!host.installed || host.via !== 'tree')
|
|
130
|
-
continue;
|
|
131
|
-
const missing = missingTreeFiles(host.id);
|
|
132
|
-
check(`${host.label} extension tree`, missing.length === 0, missing.length ? `${missing.length} missing in ${host.dir}` : '');
|
|
133
|
-
}
|
|
134
123
|
section('Usage & records');
|
|
135
124
|
console.log(` Usage DB (tersio-owned · local hosted): ${usageDbPath()}`);
|
|
136
125
|
const pricesAge = pricesMtime ? `(pulled ${relTime(Date.now() - pricesMtime.mtimeMs)} · ${absDate(pricesMtime.mtimeMs)})` : '';
|
package/cli/hosts.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// cli/hosts.ts — the agent hosts Tersio installs into, and what is on disk.
|
|
2
|
-
// One detector, shared by the doctor
|
|
3
|
-
// menu, so the three can never disagree about what is installed.
|
|
2
|
+
// One detector, shared by the doctor and both menus, so they cannot disagree.
|
|
4
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { HOME, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION } from './common.js';
|
|
@@ -33,9 +32,8 @@ function packageVersion(dir) {
|
|
|
33
32
|
return null;
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
|
-
// The tersio package pi
|
|
37
|
-
//
|
|
38
|
-
// spec, a git URL, and a local path all count when they name this package.
|
|
35
|
+
// The tersio package pi declared in its own settings.json, or null. pi
|
|
36
|
+
// identifies a source by name, so an npm spec, git URL, or path all count.
|
|
39
37
|
export function piTersioSource(agentDir = piAgentDir()) {
|
|
40
38
|
let settings;
|
|
41
39
|
try {
|
|
@@ -85,10 +83,7 @@ function detect(agentDir = piAgentDir()) {
|
|
|
85
83
|
};
|
|
86
84
|
});
|
|
87
85
|
}
|
|
88
|
-
/**
|
|
89
|
-
* The menu label, with the current state in brackets: the version when the
|
|
90
|
-
* package is already there, so a rerun shows what it would replace.
|
|
91
|
-
*/
|
|
86
|
+
/** Menu label with current state in brackets, so a rerun shows what it replaces. */
|
|
92
87
|
export function hostLabel(host) {
|
|
93
88
|
if (host.installed && host.version)
|
|
94
89
|
return `${host.label} (installed ${host.version})`;
|
|
@@ -98,9 +93,10 @@ export function hostLabel(host) {
|
|
|
98
93
|
return `${host.label} (package declared, not on disk)`;
|
|
99
94
|
return `${host.label} (not installed)`;
|
|
100
95
|
}
|
|
96
|
+
// Hints render inline, so menus name the install and doctor keeps the path.
|
|
101
97
|
export function hostHint(host) {
|
|
102
|
-
if (host.installed && host.
|
|
103
|
-
return host.
|
|
98
|
+
if (host.installed && host.via)
|
|
99
|
+
return host.via === 'tree' ? 'extension tree' : 'plugin package';
|
|
104
100
|
return host.installCmd;
|
|
105
101
|
}
|
|
106
102
|
// `--host omp|pi` from raw argv; undefined when absent. Throws on an unknown
|