@krtclcdy/tersio 2.25.1 → 2.25.3

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 (57) hide show
  1. package/README.md +1 -1
  2. package/dashboard/dist/index.html +1 -1
  3. package/{cli → dist/cli}/common.js +4 -3
  4. package/{cli → dist/cli}/dashboard.js +30 -13
  5. package/{cli → dist/cli}/doctor-fix.js +19 -30
  6. package/{cli → dist/cli}/doctor.js +19 -6
  7. package/dist/cli/headroom-install.js +266 -0
  8. package/dist/cli/headroom.js +492 -0
  9. package/{cli → dist/cli}/install.js +116 -71
  10. package/{cli → dist/cli}/interactive.js +2 -2
  11. package/{cli → dist/cli}/manifest.js +4 -1
  12. package/{cli → dist/cli}/rtk-wiring.js +5 -2
  13. package/{cli → dist/cli}/settings.js +5 -23
  14. package/{cli → dist/cli}/uninstall.js +23 -11
  15. package/{cli → dist/cli}/update.js +10 -8
  16. package/{extensions → dist/extensions}/ai-addons-updater/index.js +60 -44
  17. package/{extensions → dist/extensions}/caveman-session/index.js +11 -11
  18. package/dist/extensions/caveman-session/rule-megacave.md +63 -0
  19. package/dist/extensions/caveman-session/rule-ultra.md +63 -0
  20. package/dist/extensions/caveman-session/rule.md +79 -0
  21. package/{extensions → dist/extensions}/combo-toggle/index.js +37 -27
  22. package/dist/extensions/headroom-session/index.js +93 -0
  23. package/{extensions → dist/extensions}/lib/utils.js +27 -1
  24. package/{extensions → dist/extensions}/opencode/server.js +33 -3
  25. package/dist/extensions/shared/headroom.js +131 -0
  26. package/{extensions → dist/extensions}/shared/plugin-settings.js +1 -1
  27. package/{extensions → dist/extensions}/shared/session-state.js +14 -2
  28. package/{extensions → dist/extensions}/shared/status.js +7 -1
  29. package/{tersio.js → dist/tersio.js} +1 -1
  30. package/extensions/ai-addons-updater/index.ts +52 -40
  31. package/extensions/caveman-session/index.ts +10 -11
  32. package/extensions/caveman-session/rule-megacave.md +63 -0
  33. package/extensions/caveman-session/rule-ultra.md +63 -0
  34. package/extensions/caveman-session/rule.md +4 -15
  35. package/extensions/combo-toggle/index.ts +35 -27
  36. package/extensions/lib/utils.ts +23 -1
  37. package/extensions/opencode/server.ts +29 -3
  38. package/extensions/shared/plugin-settings.ts +1 -1
  39. package/extensions/shared/session-state.ts +15 -2
  40. package/extensions/shared/status.ts +8 -1
  41. package/package.json +17 -15
  42. package/extensions/opencode/index.js +0 -104
  43. /package/{cli → dist/cli}/banner.js +0 -0
  44. /package/{cli → dist/cli}/currency.js +0 -0
  45. /package/{cli → dist/cli}/hosts.js +0 -0
  46. /package/{cli → dist/cli}/profile.js +0 -0
  47. /package/{cli → dist/cli}/reset.js +0 -0
  48. /package/{cli → dist/cli}/usage.js +0 -0
  49. /package/{extensions → dist/extensions}/rtk-session/index.js +0 -0
  50. /package/{extensions → dist/extensions}/shared/carbon.js +0 -0
  51. /package/{extensions → dist/extensions}/shared/host.js +0 -0
  52. /package/{extensions → dist/extensions}/shared/pricing.js +0 -0
  53. /package/{extensions → dist/extensions}/shared/rtk-gain.js +0 -0
  54. /package/{extensions → dist/extensions}/shared/types.js +0 -0
  55. /package/{extensions → dist/extensions}/shared/usage-ledger.js +0 -0
  56. /package/{extensions → dist/extensions}/shared/usage-store.js +0 -0
  57. /package/{extensions → dist/extensions}/tersio-commands/index.js +0 -0
@@ -13,7 +13,7 @@ import { runReset } from './reset.js';
13
13
  import { runUsage } from './usage.js';
14
14
  import { runDashboard } from './dashboard.js';
15
15
  import { wireRtkOmp, wireRtkOpencode, wireRtkPi } from './rtk-wiring.js';
16
- import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
16
+ import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, findPackageRoot, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, ruleBodyProblem, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
17
17
  import { formatCliStatus, storedProfile, storedProfileSync, writePluginSettings } from './profile.js';
18
18
  import { runSettings } from './settings.js';
19
19
  import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
@@ -356,41 +356,53 @@ async function stepRtkSession(extDir, options) {
356
356
  console.log(' RTK session — install session mode');
357
357
  await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
358
358
  }
359
- // One fetch serves the install; dry runs stay offline and preview the bundled rule's destination.
360
- async function fetchCavemanRule(options) {
361
- const bundled = await readTextIfExists(sourcePath('caveman-session/rule.md'));
362
- if (options.dryRun)
363
- return bundled;
364
- try {
365
- return await withInteractiveSpinner('Fetching Caveman rule', () => httpsGet(CAVEMAN_REMOTE_RULE));
366
- }
367
- catch (e) {
368
- if (bundled !== null) {
369
- debug(`Using bundled Caveman rule: ${e.message}`);
370
- return bundled;
371
- }
372
- sayTagged(` [warn] Could not fetch caveman rule: ${e.message}`);
373
- return null;
374
- }
375
- }
376
- async function stepCaveman(extDir, rule, options) {
359
+ // rule.md ships with the package; the other bodies track upstream skills.
360
+ // rule.md is a deliberate fork of upstream skills/caveman/SKILL.md; a blind sync would drop the mode switch line and the STE100 rules.
361
+ const CAVEMAN_RULES = [
362
+ ['rule.md', null],
363
+ ['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
364
+ ['rule-megacave.md', CAVEMAN_REMOTE_MEGACAVE],
365
+ ];
366
+ async function stepCaveman(extDir, options) {
377
367
  if (!options.quiet)
378
- console.log(' Caveman — fetch rule and install session mode');
368
+ console.log(' Caveman — install session mode and rule bodies');
379
369
  const cavemanDir = path.join(extDir, 'caveman-session');
380
370
  if (!options.dryRun)
381
371
  await fs.mkdir(cavemanDir, { recursive: true });
382
- const ruleDest = path.join(cavemanDir, 'rule.md');
383
- if (rule === null && (await readTextIfExists(ruleDest)) !== null) {
384
- debug('Keeping existing rule.md');
385
- }
386
- else if (rule === null) {
387
- console.log(' [skip] Caveman rule.md unavailable');
388
- sayTagged(` [hint] Manual: ${CAVEMAN_REMOTE_RULE}`);
389
- }
390
- else {
391
- await writeIfChanged(ruleDest, rule, options);
372
+ for (const [name, remote] of CAVEMAN_RULES) {
373
+ const bundled = await readTextIfExists(sourcePath(`caveman-session/${name}`));
374
+ let rule = bundled;
375
+ if (remote && !options.dryRun) {
376
+ try {
377
+ const fetched = await withInteractiveSpinner(`Fetching Caveman ${name}`, () => httpsGet(remote));
378
+ const problem = ruleBodyProblem(fetched);
379
+ if (problem && bundled !== null) {
380
+ sayTagged(` [warn] ${name} fetch rejected (${problem}) — keeping the bundled body`);
381
+ rule = bundled;
382
+ }
383
+ else
384
+ rule = fetched;
385
+ }
386
+ catch (e) {
387
+ if (bundled === null)
388
+ sayTagged(` [warn] Could not fetch caveman ${name}: ${e.message}`);
389
+ else
390
+ debug(`Using bundled Caveman ${name}: ${e.message}`);
391
+ }
392
+ }
393
+ if (rule === null) {
394
+ if (remote)
395
+ sayTagged(` [hint] Manual: ${remote}`);
396
+ console.log(` [skip] Caveman ${name} unavailable`);
397
+ }
398
+ else {
399
+ await writeIfChanged(path.join(cavemanDir, name), rule, options);
400
+ }
392
401
  }
393
402
  await copySources(extDir, filesUnder('caveman-session').filter(([, to]) => to.endsWith('index.ts')), 'caveman-session/index.ts', options);
403
+ // Renamed body: drop the stale copy an older install left behind.
404
+ if (!options.dryRun)
405
+ await fs.rm(path.join(cavemanDir, 'rule-wenyan.md'), { force: true });
394
406
  }
395
407
  async function stepTersioCommands(extDir, options) {
396
408
  if (!options.quiet)
@@ -412,7 +424,7 @@ const PONYTAIL_BUNDLE_FILES = [
412
424
  'skills/ponytail/SKILL.md',
413
425
  ];
414
426
  async function stepPonytailBundle(treeDir, options) {
415
- const root = findHoistedPackage('@dietrichgebert/ponytail', path.dirname(fileURLToPath(import.meta.url)));
427
+ const root = findHoistedPackage('@dietrichgebert/ponytail', findPackageRoot(path.dirname(fileURLToPath(import.meta.url))));
416
428
  if (!root) {
417
429
  if (!options.quiet)
418
430
  console.log(' [skip] Ponytail package not found next to the CLI — OpenCode keeps the built-in text');
@@ -459,18 +471,22 @@ async function stepOpencode(options) {
459
471
  return;
460
472
  }
461
473
  const plugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
462
- const withoutTersio = plugins.filter((p) => typeof p !== 'string' || !p.includes('/opencode/plugins/tersio'));
463
- if (!withoutTersio.includes(dir))
464
- withoutTersio.push(dir);
465
- config.plugins = withoutTersio;
466
- if (!options.dryRun)
474
+ // The tree ships the rtk rewrite, so its own plugin entry is redundant.
475
+ const kept = plugins.filter((p) => typeof p !== 'string' || !p.endsWith('/opencode/plugins/rtk.ts'));
476
+ if (!kept.includes(dir))
477
+ kept.push(dir);
478
+ config.plugins = kept;
479
+ if (!options.dryRun) {
467
480
  await fs.mkdir(path.dirname(configPath), { recursive: true });
481
+ await fs.rm(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), { force: true }).catch(() => { });
482
+ }
468
483
  await writeIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`, options);
469
484
  }
470
485
  async function resolveProfile(opts = {}) {
471
486
  // Seed from the stored defaults so flag-less runs keep them.
472
487
  const profile = await storedProfile();
473
- // One numbered prompt for all three modes, for a real terminal user with no default flags. Never for --apply-update, and never for a script.
488
+ // The chosen preset owns all three modes.
489
+ let comboChosen = comboDefaultFlag !== undefined;
474
490
  if (tty() && !profileFlagsGiven && !applyUpdate) {
475
491
  const choice = await askInteractiveChoice('Session-start defaults — Combo preset', [
476
492
  { value: 'off', label: 'off' },
@@ -478,8 +494,10 @@ async function resolveProfile(opts = {}) {
478
494
  { value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
479
495
  { value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
480
496
  ], profile.comboDefault);
481
- if (choice.status === 'selected')
497
+ if (choice.status === 'selected') {
482
498
  profile.comboDefault = choice.value;
499
+ comboChosen = true;
500
+ }
483
501
  else {
484
502
  closeRL();
485
503
  process.exit(130);
@@ -488,10 +506,13 @@ async function resolveProfile(opts = {}) {
488
506
  // Explicit flags always win over the Combo preset.
489
507
  if (comboDefaultFlag !== undefined)
490
508
  profile.comboDefault = comboDefaultFlag;
491
- const preset = COMBO_PRESET_MODES[profile.comboDefault] ?? COMBO_PRESET_MODES.off;
492
- profile.cavemanDefault = preset.caveman;
493
- profile.rtkDefault = preset.rtk;
494
- profile.ponytailDefault = preset.ponytail;
509
+ // A stored per-mode choice survives installs and updates; only a chosen preset rewrites it.
510
+ if (comboChosen) {
511
+ const preset = COMBO_PRESET_MODES[profile.comboDefault] ?? COMBO_PRESET_MODES.off;
512
+ profile.cavemanDefault = preset.caveman;
513
+ profile.rtkDefault = preset.rtk;
514
+ profile.ponytailDefault = preset.ponytail;
515
+ }
495
516
  if (cavemanDefaultFlag !== undefined)
496
517
  profile.cavemanDefault = cavemanDefaultFlag;
497
518
  if (rtkDefaultFlag !== undefined)
@@ -506,7 +527,8 @@ let updatePromptDone = false;
506
527
  // Bare `tersio` at a terminal picks a command; scripts and flags install.
507
528
  async function runCommandMenu() {
508
529
  printWelcome();
509
- const newer = await checkForUpdate();
530
+ // Interactive launches always check; scripts use the cached verdict.
531
+ const newer = await checkForUpdate(true);
510
532
  if (typeof newer === 'string' && !dryRun) {
511
533
  const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
512
534
  if (answer.status === 'confirmed' && answer.value) {
@@ -636,50 +658,74 @@ async function stepPiLayer(options) {
636
658
  sayTagged(` [note] pi also has a tersio package (${declared}) — remove one, or both copies load: pi remove npm:${PACKAGE_NAME}`);
637
659
  if (!options.quiet)
638
660
  console.log(` Pi — write the extension tree (${extDir})`);
639
- const cavemanRule = await fetchCavemanRule(options);
640
661
  await stepSharedSessionState(extDir, options);
641
662
  // OMP loads rtk-session from its plugin manifest, so only the pi tree copies it.
642
663
  await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
643
- await stepCaveman(extDir, cavemanRule, options);
664
+ await stepCaveman(extDir, options);
644
665
  await stepCombo(extDir, options);
645
666
  await stepTersioCommands(extDir, options);
646
667
  await stepUpdater(extDir, options);
647
668
  await stepPonytailForPi(agentDir, options);
648
669
  }
649
- // Ponytail for pi: install the package, or copy the bundled skills offline.
670
+ // A local dir, not an npm spec: pi only version-checks npm entries, so this avoids the update nag.
671
+ const PI_PONYTAIL_FILES = [
672
+ 'pi-extension/index.js',
673
+ 'hooks/ponytail-instructions.js',
674
+ 'hooks/ponytail-config.js',
675
+ ...['ponytail', 'ponytail-audit', 'ponytail-debt', 'ponytail-gain', 'ponytail-help', 'ponytail-review']
676
+ .map((skill) => `skills/${skill}/SKILL.md`),
677
+ ];
678
+ const PI_PONYTAIL_PACKAGE = `${JSON.stringify({ name: 'ponytail', version: '0.0.0', private: true, pi: { extensions: ['./pi-extension/index.js'], skills: ['./skills'] } }, null, 2)}\n`;
650
679
  async function stepPonytailForPi(agentDir, options) {
651
680
  if (!options.quiet)
652
- console.log(' Ponytail — ensure the pi package');
653
- if (!options.dryRun) {
681
+ console.log(' Ponytail — write the local pi package');
682
+ const root = findHoistedPackage('@dietrichgebert/ponytail', findPackageRoot(path.dirname(fileURLToPath(import.meta.url))));
683
+ const dest = path.join(agentDir, 'ponytail');
684
+ if (!root) {
685
+ console.log(' [skip] Ponytail package not found next to the CLI');
686
+ console.log(' [hint] The /ponytail command will not work; run: tersio update --host pi');
687
+ return;
688
+ }
689
+ await writeIfChanged(path.join(dest, 'package.json'), PI_PONYTAIL_PACKAGE, options);
690
+ for (const rel of PI_PONYTAIL_FILES) {
691
+ let text;
654
692
  try {
655
- await execNetwork('Installing Ponytail for pi', 'pi', ['install', 'npm:@dietrichgebert/ponytail'], { timeout: 300000 });
656
- return;
693
+ text = await fs.readFile(path.join(root, ...rel.split('/')), 'utf8');
657
694
  }
658
- catch (e) {
659
- sayTagged(` [fail] pi install ponytail: ${shortError(e)}`);
695
+ catch {
696
+ sayTagged(` [fail] Ponytail file missing: ${rel}`);
697
+ return;
660
698
  }
699
+ await writeIfChanged(path.join(dest, ...rel.split('/')), text, options);
661
700
  }
662
- const skills = findHoistedPackage('@dietrichgebert/ponytail', path.dirname(fileURLToPath(import.meta.url)), 'skills');
663
- if (!skills) {
664
- console.log(' [skip] Ponytail package not found next to the CLI — skills not copied');
665
- console.log(' [hint] The /ponytail command will not work; run: pi install npm:@dietrichgebert/ponytail');
701
+ if (options.dryRun)
666
702
  return;
703
+ try {
704
+ await execNetwork('Removing the pi npm copy of Ponytail', 'pi', ['remove', 'npm:@dietrichgebert/ponytail'], { timeout: 120000 });
667
705
  }
668
- const dest = path.join(agentDir, 'skills');
669
- if (options.dryRun) {
670
- if (verbose && !options.quiet)
671
- sayTagged(` [dry-run] would copy Ponytail skills to ${dest}`);
672
- return;
706
+ catch (e) {
707
+ sayTagged(` [warn] pi remove ponytail: ${shortError(e)}`);
673
708
  }
674
- for (const entry of await fs.readdir(skills, { withFileTypes: true })) {
675
- if (!entry.isDirectory())
676
- continue;
677
- const to = path.join(dest, entry.name);
678
- await fs.rm(to, { recursive: true, force: true });
679
- await fs.cp(path.join(skills, entry.name), to, { recursive: true });
680
- if (!options.quiet)
681
- sayTagged(` [write] skill ${entry.name} → ${to}`);
709
+ await registerPiLocalPonytail(agentDir, dest, options);
710
+ }
711
+ // pi lists package sources in settings; swap the npm spec for the local dir.
712
+ async function registerPiLocalPonytail(agentDir, dest, options) {
713
+ const settingsPath = path.join(agentDir, 'settings.json');
714
+ let settings;
715
+ try {
716
+ settings = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
717
+ }
718
+ catch {
719
+ return;
682
720
  }
721
+ const packages = (Array.isArray(settings.packages) ? settings.packages : []);
722
+ const kept = packages.filter((p) => typeof p !== 'string' || !p.startsWith('npm:@dietrichgebert/ponytail'));
723
+ if (!kept.includes(dest))
724
+ kept.push(dest);
725
+ settings.packages = kept;
726
+ await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
727
+ if (!options.quiet)
728
+ sayTagged(` [write] pi package source → ${dest}`);
683
729
  }
684
730
  async function runInstall() {
685
731
  // Without this guard a picked command re-enters here with command === null and loops straight back into the picker.
@@ -799,14 +845,13 @@ async function runInstall() {
799
845
  closeRL();
800
846
  return;
801
847
  }
802
- const cavemanRule = await fetchCavemanRule(installOptions);
803
848
  await capture('shared', () => stepSharedSessionState(userExtDir, installOptions));
804
849
  let selfPlugin = false;
805
850
  await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, installOptions); });
806
851
  await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
807
852
  await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
808
853
  await capture('rtk session', () => stepRtkSession(userExtDir, installOptions));
809
- await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'), cavemanRule, installOptions));
854
+ await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'), installOptions));
810
855
  await capture('combo', () => stepCombo(userExtDir, installOptions));
811
856
  await capture('commands', () => stepTersioCommands(userExtDir, installOptions));
812
857
  await capture('updater', () => stepUpdater(userExtDir, installOptions));
@@ -26,9 +26,9 @@ function tty() {
26
26
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
27
27
  }
28
28
  let spinnerDepth = 0;
29
- // Run async work under a TTY-only timer spinner. The spinner is cleared before the caller prints its normal result line, preserving locked output shapes.
29
+ // Clack's spinner stop writes an extra newline under CI, shifting later output by a row.
30
30
  async function withInteractiveSpinner(message, work) {
31
- if (!tty() || spinnerDepth > 0)
31
+ if (!tty() || spinnerDepth > 0 || process.env.CI === 'true')
32
32
  return work(() => { });
33
33
  // Clack manages stdin itself. Close the legacy question interface before its first use; non-interactive callers never reach this branch.
34
34
  closeRL();
@@ -1,7 +1,8 @@
1
1
  // Single list of install writes, shared by installer, doctor, and tests.
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- export const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
4
+ import { findPackageRoot } from '../extensions/lib/utils.js';
5
+ export const EXT_DIR = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'extensions');
5
6
  /** Root entrypoint OpenCode resolves for the installed tree; re-exports the versioned entry beside it. Built by join so the path never reads as a relative import (repo-integrity scans literals). */
6
7
  const SERVER_SPEC = `./${['opencode', 'server.ts'].join('/')}`;
7
8
  export const OPENCODE_SERVER_SHIM = `// Generated by \`tersio install --host opencode\`: OpenCode resolves <pluginDir>/server.ts.
@@ -20,6 +21,8 @@ export const TREE_FILES = [
20
21
  'lib/utils.ts',
21
22
  'caveman-session/index.ts',
22
23
  'caveman-session/rule.md',
24
+ 'caveman-session/rule-ultra.md',
25
+ 'caveman-session/rule-megacave.md',
23
26
  'rtk-session/index.ts',
24
27
  'combo-toggle/index.ts',
25
28
  'tersio-commands/index.ts',
@@ -1,5 +1,5 @@
1
1
  // OMP loads only listed extensions, so register rtk's path; fail-open.
2
- import { promises as fs } from 'node:fs';
2
+ import { existsSync, promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { execFile } from 'node:child_process';
@@ -47,8 +47,11 @@ export async function wireRtkOmp(rtkBin, options = {}) {
47
47
  await ensureRtkInConfig(options);
48
48
  return true;
49
49
  }
50
+ // The Tersio plugin does the same rewrite, so OpenCode needs no rtk plugin of its own.
50
51
  export async function wireRtkOpencode(rtkBin, options = {}) {
51
- return runWire(rtkBin, { host: 'OpenCode', mechanism: 'tool.execute.before', initArgs: ['init', '-g', '--opencode'], hint: 'rtk init -g --opencode' }, options);
52
+ if (!options.dryRun && !options.quiet)
53
+ console.log(' RTK rewrite runs inside the Tersio plugin — no separate entry');
54
+ return existsSync(rtkBin);
52
55
  }
53
56
  export async function wireRtkPi(rtkBin, options = {}) {
54
57
  return runWire(rtkBin, { host: 'pi', mechanism: 'bash tool_call', initArgs: ['init', '-g', '--agent', 'pi'], hint: 'rtk init -g --agent pi' }, options);
@@ -1,6 +1,6 @@
1
1
  // cli/settings.ts — view/edit session-start defaults and the display currency.
2
- import path from 'node:path';
3
- import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, subagentMarkerFlags, subagentMarkersGiven, } from './common.js';
2
+ import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, subagentMarkerFlags, subagentMarkersGiven, } from './common.js';
3
+ import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
4
4
  import { CURRENCY_CODES } from './currency.js';
5
5
  import { textTable } from './usage.js';
6
6
  import { ask, askInteractiveChoice, closeRL, tty } from './interactive.js';
@@ -45,25 +45,6 @@ async function askProfile(current) {
45
45
  backupSchedule: current.backupSchedule,
46
46
  subagentMarkers: current.subagentMarkers,
47
47
  };
48
- const caveman = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
49
- value: v, label: v,
50
- })), next.cavemanDefault);
51
- if (caveman.status !== 'selected')
52
- return null;
53
- next.cavemanDefault = caveman.value;
54
- const rtk = await askInteractiveChoice('RTK default', [
55
- { value: 'on', label: 'on' },
56
- { value: 'off', label: 'off' },
57
- ], next.rtkDefault ? 'on' : 'off');
58
- if (rtk.status !== 'selected')
59
- return null;
60
- next.rtkDefault = rtk.value === 'on';
61
- const ponytail = await askInteractiveChoice('Ponytail default', [...PONYTAIL_DEFAULTS].map((v) => ({
62
- value: v, label: v,
63
- })), next.ponytailDefault);
64
- if (ponytail.status !== 'selected')
65
- return null;
66
- next.ponytailDefault = ponytail.value;
67
48
  const cur = await askInteractiveChoice('Display currency (usage/dashboard reports)', CURRENCY_CODES.map((v) => ({
68
49
  value: v, label: v,
69
50
  })), next.currency);
@@ -103,7 +84,7 @@ async function askDiagSchedule(current) {
103
84
  }
104
85
  const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis', 'markers'];
105
86
  function settingsUsage() {
106
- console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis|markers] [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--subagent-marker <text>]... [--dry-run]');
87
+ console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis|markers] [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|megacave-lite|megacave-full|megacave-ultra] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--subagent-marker <text>]... [--dry-run]');
107
88
  }
108
89
  // Single-setting jump: `tersio settings diagnosis` prompts only that value instead of walking the whole chain. Returns false when aborted.
109
90
  async function runSingleSetting(name, current, nextDiag) {
@@ -198,7 +179,8 @@ function printSettingsTable(current) {
198
179
  for (const l of textTable(['Setting', 'Current', 'Valid values'], rows, [false, false, false], 64)) {
199
180
  console.log(l);
200
181
  }
201
- console.log(` Stored: ${PACKAGE_NAME} in ${path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json')}`);
182
+ console.log(` Stored: ${tersioSettingsFile()}`);
183
+ console.log(` Combo ${current.comboDefault} sets caveman/rtk/ponytail. Tune one: tersio settings <name>`);
202
184
  }
203
185
  async function runSettings() {
204
186
  const current = await storedProfile();
@@ -4,8 +4,7 @@ import path from 'node:path';
4
4
  import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, RTK_BINARY_NAME, args, dryRun, keepPonytail, removePonytail, removeRtk, yes, debug, writeConfigLines, } from './common.js';
5
5
  import { askInteractiveChoice, closeRL, confirmDestructive, tty, sayTagged } from './interactive.js';
6
6
  import { detectHosts, hostHint, hostLabel, parseHostArg } from './hosts.js';
7
- import { findHoistedPackage, piAgentDir, readTextIfExists } from '../extensions/lib/utils.js';
8
- import { fileURLToPath } from 'node:url';
7
+ import { piAgentDir, readTextIfExists } from '../extensions/lib/utils.js';
9
8
  import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
10
9
  // The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a shared binary are not ours to delete.
11
10
  const PONYTAIL_PKG = '@dietrichgebert/ponytail';
@@ -42,20 +41,30 @@ async function piRemove(spec, shouldDryRun) {
42
41
  sayTagged(` [hint] Manual: pi remove ${spec}`);
43
42
  }
44
43
  }
45
- // Only the skill directories the installer copied, never pi's own skills dir.
44
+ // The installer writes a local package dir; drop it and its settings entry.
46
45
  async function removeCopiedPonytailSkills(shouldDryRun) {
47
- const source = findHoistedPackage(PONYTAIL_PKG, path.dirname(fileURLToPath(import.meta.url)), 'skills');
48
- if (!source)
49
- return;
50
- for (const entry of await fs.readdir(source, { withFileTypes: true })) {
51
- if (entry.isDirectory())
52
- await removeUninstallTarget(path.join(piAgentDir(), 'skills', entry.name), shouldDryRun);
46
+ const agentDir = piAgentDir();
47
+ const settingsPath = path.join(agentDir, 'settings.json');
48
+ try {
49
+ const settings = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
50
+ if (!Array.isArray(settings.packages))
51
+ return;
52
+ const kept = settings.packages.filter((p) => p !== path.join(agentDir, 'ponytail'));
53
+ if (kept.length === settings.packages.length)
54
+ return;
55
+ if (shouldDryRun)
56
+ sayTagged(` [dry-run] would drop the local ponytail entry from ${settingsPath}`);
57
+ else
58
+ await fs.writeFile(settingsPath, `${JSON.stringify({ ...settings, packages: kept }, null, 2)}\n`, 'utf8');
53
59
  }
60
+ catch { /* no settings file, nothing to clean */ }
61
+ await removeUninstallTarget(path.join(agentDir, 'ponytail'), shouldDryRun);
54
62
  }
55
63
  // pi removal delegates to pi remove; always list and confirm first.
56
64
  async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRemoveRtk, confirmed) {
57
65
  const targets = PI_TREE_DIRS.map((dir) => path.join(piAgentDir(), 'extensions', dir));
58
66
  const rtkWiring = path.join(piAgentDir(), 'extensions', 'rtk.ts');
67
+ const piPonytail = path.join(piAgentDir(), 'ponytail');
59
68
  if (!host.installed && !host.declared) {
60
69
  sayTagged(` [skip] ${host.label} — nothing installed (${host.installCmd})`);
61
70
  return false;
@@ -63,8 +72,10 @@ async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRem
63
72
  console.log('Will remove:');
64
73
  for (const t of targets)
65
74
  console.log(` ${t}`);
66
- if (shouldRemovePonytail)
75
+ if (shouldRemovePonytail) {
67
76
  console.log(` npm:${PONYTAIL_PKG} (pi package)`);
77
+ console.log(` ${piPonytail} (local Ponytail package)`);
78
+ }
68
79
  if (shouldRemoveRtk)
69
80
  console.log(` ${rtkWiring} (rtk wiring)`);
70
81
  console.log(` ${tersioSettingsFile()} (session defaults)`);
@@ -278,7 +289,8 @@ async function runUninstall(options = {}) {
278
289
  }
279
290
  }
280
291
  // Cleared last: writeConfigLines above would just make a fresh one.
281
- for (const backup of [`${configPath}.bak`, path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'extensions', 'caveman-session', 'rule.md.bak')]) {
292
+ const cavemanDir = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'extensions', 'caveman-session');
293
+ for (const backup of [`${configPath}.bak`, path.join(cavemanDir, 'rule.md.bak'), path.join(cavemanDir, 'rule-ultra.md.bak'), path.join(cavemanDir, 'rule-megacave.md.bak')]) {
282
294
  await removeUninstallTarget(backup, shouldDryRun, false);
283
295
  }
284
296
  // Remove the bundled Ponytail copy; its config.yml entry is already filtered.
@@ -6,7 +6,7 @@ import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION
6
6
  import { execNetwork, sayTagged } from './interactive.js';
7
7
  import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/lib/utils.js';
8
8
  import { refreshPrices } from '../extensions/shared/pricing.js';
9
- import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
9
+ import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
10
10
  import { parseHostArg } from './hosts.js';
11
11
  const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
12
12
  function newerThan(a, b) {
@@ -78,20 +78,23 @@ async function execInherit(cmd, args) {
78
78
  async function probeUpdatePlan(cliLatest) {
79
79
  const rtkBin = resolveRtkBinary();
80
80
  const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
81
- const tersioRule = path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman-session', 'rule.md');
81
+ // rule.md ships with the release; only the tracked bodies can go stale.
82
+ const tersioRule = (name) => path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman-session', name);
82
83
  const controller = new AbortController();
83
- const [rtkLocal, rtkRelease, ruleLocalText, ruleRemoteText, ponytailLocalText] = await Promise.all([
84
+ const [rtkLocal, rtkRelease, ruleLocal, ruleRemote, ponytailLocalText] = await Promise.all([
84
85
  settle(rtkBin ? execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null) : Promise.resolve(null), 6000),
85
86
  settle(httpsGet(RTK_RELEASE_API, { signal: controller.signal }).then((text) => normalizeRtkVersion(JSON.parse(text).tag_name) || null), 1500),
86
- readTextIfExists(tersioRule),
87
- settle(httpsGet(CAVEMAN_REMOTE_RULE, { signal: controller.signal }).then((text) => sha256Hex(text).slice(0, 8)), 1500),
87
+ Promise.all([readTextIfExists(tersioRule('rule-ultra.md')), readTextIfExists(tersioRule('rule-megacave.md'))])
88
+ .then((texts) => (texts.every(Boolean) ? texts.map((t) => sha256Hex(t).slice(0, 8)).join('+') : null)),
89
+ settle(Promise.all([httpsGet(CAVEMAN_REMOTE_ULTRA, { signal: controller.signal }), httpsGet(CAVEMAN_REMOTE_MEGACAVE, { signal: controller.signal })])
90
+ .then((texts) => texts.map((t) => sha256Hex(t).slice(0, 8)).join('+')), 1500),
88
91
  readTextIfExists(ponytailPkg),
89
92
  ]);
90
93
  controller.abort();
91
94
  return {
92
95
  cli: cliLatest,
93
96
  rtk: [rtkLocal, rtkRelease],
94
- rule: [ruleLocalText ? sha256Hex(ruleLocalText).slice(0, 8) : null, ruleRemoteText],
97
+ rule: [ruleLocal, ruleRemote],
95
98
  ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
96
99
  };
97
100
  }
@@ -211,8 +214,7 @@ async function runLatestUpdate() {
211
214
  // Inherited stdio, no outer spinner; the delegated installer runs quiet and this parent owns both the plan line and the closing summary.
212
215
  try {
213
216
  await execInherit(npmCommand, npmCommandArgs);
214
- const hostName = pinned === 'pi' ? 'pi' : pinned === 'opencode' ? 'OpenCode' : 'OMP';
215
- console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart ${hostName}.`);
217
+ console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION} and add-ons updated.`);
216
218
  if (!dryRun) {
217
219
  try {
218
220
  await refreshPrices();