@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.
Files changed (49) hide show
  1. package/README.md +1 -3
  2. package/cli/banner.js +2 -3
  3. package/cli/currency.js +3 -7
  4. package/cli/dashboard.js +167 -3
  5. package/cli/doctor-fix.js +15 -12
  6. package/cli/doctor.js +1 -12
  7. package/cli/hosts.js +7 -11
  8. package/cli/install.js +113 -71
  9. package/cli/interactive.js +36 -2
  10. package/cli/layer.js +1 -3
  11. package/cli/manifest.js +1 -4
  12. package/cli/profile.js +9 -7
  13. package/cli/reset.js +3 -7
  14. package/cli/rtk-wiring.js +1 -6
  15. package/cli/settings.js +3 -8
  16. package/cli/uninstall.js +43 -24
  17. package/cli/update.js +7 -8
  18. package/cli/usage.js +15 -5
  19. package/dashboard/dist/index.html +18 -18
  20. package/extensions/ai-addons-updater/index.js +6 -5
  21. package/extensions/ai-addons-updater/index.ts +7 -6
  22. package/extensions/caveman-session/index.js +14 -29
  23. package/extensions/caveman-session/index.ts +15 -30
  24. package/extensions/combo-toggle/index.js +10 -14
  25. package/extensions/combo-toggle/index.ts +10 -15
  26. package/extensions/lib/utils.js +4 -0
  27. package/extensions/lib/utils.ts +3 -0
  28. package/extensions/rtk-session/index.js +27 -31
  29. package/extensions/rtk-session/index.ts +25 -29
  30. package/extensions/shared/carbon.js +3 -8
  31. package/extensions/shared/carbon.ts +4 -8
  32. package/extensions/shared/host.js +1 -11
  33. package/extensions/shared/host.ts +1 -11
  34. package/extensions/shared/plugin-settings.js +4 -14
  35. package/extensions/shared/plugin-settings.ts +4 -14
  36. package/extensions/shared/pricing.js +4 -6
  37. package/extensions/shared/pricing.ts +4 -6
  38. package/extensions/shared/rtk-gain.js +2 -7
  39. package/extensions/shared/rtk-gain.ts +2 -7
  40. package/extensions/shared/session-state.js +30 -18
  41. package/extensions/shared/session-state.ts +33 -16
  42. package/extensions/shared/usage-ledger.js +138 -17
  43. package/extensions/shared/usage-ledger.ts +143 -19
  44. package/extensions/shared/usage-store.js +175 -23
  45. package/extensions/shared/usage-store.ts +171 -22
  46. package/extensions/tersio-commands/index.js +1 -5
  47. package/extensions/tersio-commands/index.ts +1 -5
  48. package/package.json +6 -7
  49. package/tersio.js +1 -3
package/README.md CHANGED
@@ -8,9 +8,7 @@
8
8
  [![Build](https://shieldcn.dev/github/ci/KurutoDenzeru/tersio.svg?variant=branded&size=xs&logo=githubactions&label=Build)](https://github.com/KurutoDenzeru/tersio/actions)
9
9
  [![MIT](https://shieldcn.dev/badge/license-MIT-2563eb.svg?variant=branded&size=xs&logo=opensourceinitiative)](./LICENSE)
10
10
 
11
- Token-saving add-ons for Oh My Pi and pi: terse caveman replies, compact
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 (16x12 px -> 6 half-block rows).
2
- // OMP-style launch moment for interactive runs: clack intro + note box.
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 — display-currency helpers (USD-base, offline snapshot rates).
2
- // Same 10 currencies, symbols, and snapshot rates as the dashboard's
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 from the omp plugin lock file (written by tersio settings);
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
- 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');
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
- console.log(` [warn] bundled source missing: ${from}`);
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
- console.log(` [ok] ${host.label} extension tree: ${dest}`);
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
- for (const [from, to] of TREE_FILES.map((file) => [sourcePath(file), file])) {
43
- const text = await readTextIfExists(from);
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
- console.log(` [warn] bundled source missing: ${from}`);
47
+ sayTagged(` [warn] bundled source missing: ${sourcePath(file)}`);
46
48
  else
47
- await writeIfChanged(path.join(pluginExtDir, to), text, { dryRun, verbose });
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
- console.log(` [dry-run] would create ${configPath}`);
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
- console.log(` [dry-run] would register ${PACKAGE_NAME} in ${pkgPath}`);
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
- console.log(` [dry-run] would download a checksum-verified rtk to ${binDest} and wire it into OMP`);
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
- console.log(` [ok] ${label}`);
186
+ sayTagged(` [ok] ${label}`);
184
187
  }
185
188
  catch (e) {
186
189
  failed.push(label);
187
- console.log(` [fail] ${label}: ${(e.message || '').slice(0, 200)}`);
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, missingTreeFiles, ompPackageDir } from './hosts.js';
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 rows, the install menu, and the uninstall
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 has declared in its own settings.json, or null. pi
37
- // writes the declaration on install and identifies a source by name, so an npm
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.dir)
103
- return host.dir;
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