@krtclcdy/tersio 2.25.2 → 2.25.4

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 +3 -3
  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 +2 -4
  6. package/{cli → dist/cli}/doctor.js +13 -6
  7. package/{cli → dist/cli}/install.js +56 -15
  8. package/{cli → dist/cli}/manifest.js +4 -2
  9. package/{cli → dist/cli}/settings.js +5 -23
  10. package/{cli → dist/cli}/uninstall.js +1 -1
  11. package/{cli → dist/cli}/update.js +4 -5
  12. package/{extensions → dist/extensions}/ai-addons-updater/index.js +4 -4
  13. package/{extensions → dist/extensions}/caveman-session/index.js +12 -5
  14. package/dist/extensions/caveman-session/rule-ultra.md +63 -0
  15. package/dist/extensions/caveman-session/rule.md +79 -0
  16. package/{extensions → dist/extensions}/combo-toggle/index.js +39 -29
  17. package/{extensions → dist/extensions}/lib/utils.js +26 -1
  18. package/{extensions → dist/extensions}/opencode/server.js +45 -1
  19. package/dist/extensions/rtk-filter/index.js +87 -0
  20. package/{extensions → dist/extensions}/rtk-session/index.js +3 -2
  21. package/{extensions → dist/extensions}/shared/host.js +18 -12
  22. package/{extensions → dist/extensions}/shared/plugin-settings.js +1 -1
  23. package/{extensions → dist/extensions}/shared/session-state.js +17 -3
  24. package/{extensions → dist/extensions}/shared/status.js +7 -1
  25. package/{tersio.js → dist/tersio.js} +1 -1
  26. package/extensions/ai-addons-updater/index.ts +4 -5
  27. package/extensions/caveman-session/index.ts +12 -5
  28. package/extensions/caveman-session/rule-megacave.md +63 -0
  29. package/extensions/caveman-session/rule.md +4 -2
  30. package/extensions/combo-toggle/index.ts +37 -29
  31. package/extensions/lib/utils.ts +22 -1
  32. package/extensions/opencode/server.ts +38 -1
  33. package/extensions/rtk-filter/index.ts +90 -0
  34. package/extensions/rtk-session/index.ts +3 -2
  35. package/extensions/shared/host.ts +26 -13
  36. package/extensions/shared/plugin-settings.ts +1 -1
  37. package/extensions/shared/session-state.ts +18 -3
  38. package/extensions/shared/status.ts +8 -1
  39. package/extensions/shared/types.ts +10 -2
  40. package/package.json +16 -15
  41. package/extensions/opencode/index.js +0 -104
  42. /package/{cli → dist/cli}/banner.js +0 -0
  43. /package/{cli → dist/cli}/currency.js +0 -0
  44. /package/{cli → dist/cli}/hosts.js +0 -0
  45. /package/{cli → dist/cli}/interactive.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}/rtk-wiring.js +0 -0
  49. /package/{cli → dist/cli}/usage.js +0 -0
  50. /package/{extensions/caveman-session/rule-wenyan.md → dist/extensions/caveman-session/rule-megacave.md} +0 -0
  51. /package/{extensions → dist/extensions}/shared/carbon.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
@@ -4,7 +4,8 @@ import { promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { createRequire } from 'node:module';
7
- import { readTextIfExists } from '../extensions/lib/utils.js';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { findPackageRoot, readTextIfExists } from '../extensions/lib/utils.js';
8
9
  import { parseCurrencyFlag, readStoredCurrency } from './currency.js';
9
10
  const IS_WINDOWS = process.platform === 'win32';
10
11
  const RTK_BINARY_NAME = IS_WINDOWS ? 'rtk.exe' : 'rtk';
@@ -15,8 +16,8 @@ const BUN_BIN_DIR = path.join(HOME, '.bun', 'bin');
15
16
  const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
16
17
  const PACKAGE_NAME = '@krtclcdy/tersio';
17
18
  const PACKAGE_BIN = 'tersio';
18
- const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('../package.json');
19
- const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
19
+ const { version: PACKAGE_VERSION } = createRequire(import.meta.url)(path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'package.json'));
20
+ const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'megacave-lite', 'megacave-full', 'megacave-ultra']);
20
21
  const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra']);
21
22
  const RTK_DEFAULTS = new Set(['on', 'off']);
22
23
  const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
@@ -16,8 +16,8 @@ import { BACKUP_SCHEDULES, formatCliStatus, storedProfile, writePluginSettings }
16
16
  import { normalizeComboLevel } from '../extensions/shared/session-state.js';
17
17
  import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS } from './common.js';
18
18
  import { PACKAGE_NAME } from './common.js';
19
- import { resolveRtkBinary } from '../extensions/lib/utils.js';
20
- const DASHBOARD_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard', 'dist');
19
+ import { findPackageRoot, resolveRtkBinary } from '../extensions/lib/utils.js';
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');
23
23
  function requireDashboardBundle() {
@@ -634,17 +634,34 @@ async function runDashboard(options) {
634
634
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
635
635
  res.end(await readSegment(DASHBOARD_INDEX));
636
636
  });
637
- server.listen(options.port, '127.0.0.1', () => {
638
- try {
639
- process.title = 'tersio dashboard';
640
- }
641
- catch { /* non-POSIX shells keep node */ }
642
- const address = server.address();
643
- const port = typeof address === 'object' && address ? address.port : options.port;
644
- const url = `http://127.0.0.1:${port}`;
645
- console.log(`[ok] Dashboard live → ${url} (Ctrl-C to stop)`);
646
- if (options.open)
647
- openBrowser(url);
637
+ await new Promise((resolve, reject) => {
638
+ let stopping = false;
639
+ const rejectOnce = (err) => {
640
+ if (!stopping)
641
+ reject(err);
642
+ };
643
+ const stop = () => {
644
+ if (stopping)
645
+ return;
646
+ stopping = true;
647
+ server.close(() => resolve());
648
+ server.closeIdleConnections();
649
+ };
650
+ process.once('SIGINT', stop);
651
+ process.once('SIGTERM', stop);
652
+ server.once('error', rejectOnce);
653
+ server.listen(options.port, '127.0.0.1', () => {
654
+ try {
655
+ process.title = 'tersio dashboard';
656
+ }
657
+ catch { /* non-POSIX shells keep node */ }
658
+ const address = server.address();
659
+ const port = typeof address === 'object' && address ? address.port : options.port;
660
+ const url = `http://127.0.0.1:${port}`;
661
+ console.log(`[ok] Dashboard live → ${url} (Ctrl-C to stop)`);
662
+ if (options.open)
663
+ openBrowser(url);
664
+ });
648
665
  });
649
666
  }
650
667
  export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule };
@@ -1,15 +1,13 @@
1
1
  import { existsSync, promises as fs } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
4
  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
5
  import { execNetwork, sayTagged } from './interactive.js';
7
6
  import { wireRtkOmp, wireRtkOpencode, wireRtkPi, ensureRtkInConfig } from './rtk-wiring.js';
8
7
  import { RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
9
8
  import { runLatestUpdate } from './update.js';
10
- import { OPENCODE_SERVER_SHIM, TREE_FILES, sourcePath } from './manifest.js';
9
+ import { EXT_DIR, OPENCODE_SERVER_SHIM, TREE_FILES, sourcePath } from './manifest.js';
11
10
  import { detectHosts, hostExtensionsDir } from './hosts.js';
12
- const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
13
11
  // A written tree is the install for both hosts now, so --fix restores it for whichever hosts have it, alongside the OMP package when that is present.
14
12
  async function fixExtensionTrees() {
15
13
  // The opencode entrypoint is foreign to pi/omp loaders: never copy it there, and remove strays.
@@ -148,7 +146,7 @@ async function fixRtk(binDir) {
148
146
  console.log(' Doctor --fix: repairing RTK binary + wiring');
149
147
  const binDest = path.join(binDir, RTK_BINARY_NAME);
150
148
  if (dryRun) {
151
- sayTagged(` [dry-run] would download a checksum-verified rtk to ${binDest} and wire it into OMP`);
149
+ sayTagged(` [dry-run] would download a checksum-verified rtk to ${binDest} and wire it into the installed hosts`);
152
150
  return;
153
151
  }
154
152
  const spec = rtkPlatformSpec();
@@ -6,7 +6,7 @@ import { detectHosts, hostExtensionsDir, 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';
9
- import { readTextIfExists, resolveRtkBinary } from '../extensions/lib/utils.js';
9
+ import { readTextIfExists, resolveRtkBinary, ruleBodyProblem } from '../extensions/lib/utils.js';
10
10
  import { TREE_FILES } from './manifest.js';
11
11
  async function runDoctor(recheck = false) {
12
12
  console.log('\n=== Tersio Doctor ===');
@@ -30,7 +30,7 @@ async function runDoctor(recheck = false) {
30
30
  cavemanIndexText: readTextIfExists(cavemanIndex),
31
31
  cavemanRuleText: readTextIfExists(cavemanRule('rule.md')),
32
32
  cavemanUltraText: readTextIfExists(cavemanRule('rule-ultra.md')),
33
- cavemanWenyanText: readTextIfExists(cavemanRule('rule-wenyan.md')),
33
+ cavemanMegacaveText: readTextIfExists(cavemanRule('rule-megacave.md')),
34
34
  rtkIndexText: readTextIfExists(rtkIndex),
35
35
  updaterIndexText: readTextIfExists(updaterIndex),
36
36
  rtkMtime: rtkBin ? fs.stat(rtkBin).catch(() => null) : Promise.resolve(null),
@@ -42,7 +42,7 @@ async function runDoctor(recheck = false) {
42
42
  const err = e;
43
43
  return err.stdout?.trim() || err.stderr?.trim() || null;
44
44
  }) : Promise.resolve(null);
45
- const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanUltraText, cavemanWenyanText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
45
+ const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanUltraText, cavemanMegacaveText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
46
46
  Promise.all([
47
47
  probes.ompPkgText,
48
48
  probes.configText,
@@ -56,7 +56,7 @@ async function runDoctor(recheck = false) {
56
56
  Promise.all([
57
57
  probes.cavemanRuleText,
58
58
  probes.cavemanUltraText,
59
- probes.cavemanWenyanText,
59
+ probes.cavemanMegacaveText,
60
60
  probes.ruleMtime,
61
61
  probes.rtkBinText,
62
62
  probes.rtkMtime,
@@ -133,9 +133,16 @@ async function runDoctor(recheck = false) {
133
133
  check('Prices feed', pricesMtime !== null, pricesAge || pricesCachePath());
134
134
  section('Add-ons');
135
135
  const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
136
- const cavemanRules = [['rule.md', cavemanRuleText], ['rule-ultra.md', cavemanUltraText], ['rule-wenyan.md', cavemanWenyanText]];
136
+ const cavemanRules = [['rule.md', cavemanRuleText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
137
137
  const missingRules = cavemanRules.filter(([, text]) => text === null).map(([name]) => name);
138
138
  check('Caveman rule', missingRules.length === 0, missingRules.length ? `missing: ${missingRules.join(', ')}` : ruleAge);
139
+ for (const [name, text] of cavemanRules) {
140
+ if (text === null)
141
+ continue;
142
+ const problem = ruleBodyProblem(text);
143
+ if (problem)
144
+ warnLine(`Caveman ${name}`, `unusable body (${problem}) — run: tersio install`);
145
+ }
139
146
  const rtkAge = rtkMtime ? `(updated ${relTime(Date.now() - rtkMtime.mtimeMs)} · ${absDate(rtkMtime.mtimeMs)})` : '';
140
147
  check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
141
148
  // rtk_run needs a host exec API, which doctor cannot assume.
@@ -245,7 +252,7 @@ async function runDoctorFix(tally) {
245
252
  console.log('\n Doctor --fix: repairs done — rechecking.');
246
253
  const again = await runDoctor(true);
247
254
  if (again.missing + again.warn === 0)
248
- console.log(' Doctor --fix: all checks pass. Restart OMP.');
255
+ console.log(' Doctor --fix: all checks pass. Restart your agent.');
249
256
  else
250
257
  console.log(` Doctor --fix: still ${again.missing} missing + ${again.warn} warn — rerun or repair manually.`);
251
258
  }
@@ -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_ULTRA, CAVEMAN_REMOTE_WENYAN, 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,11 +356,18 @@ 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
+ // pi's built-in grep and glob never reach the bash hook, so this one filters their output.
360
+ async function stepRtkFilter(extDir, options) {
361
+ if (!options.quiet)
362
+ console.log(' RTK filter — install built-in tool output filtering');
363
+ await copySources(extDir, filesUnder('rtk-filter'), 'rtk-filter/index.ts', options);
364
+ }
359
365
  // rule.md ships with the package; the other bodies track upstream skills.
366
+ // 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.
360
367
  const CAVEMAN_RULES = [
361
368
  ['rule.md', null],
362
369
  ['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
363
- ['rule-wenyan.md', CAVEMAN_REMOTE_WENYAN],
370
+ ['rule-megacave.md', CAVEMAN_REMOTE_MEGACAVE],
364
371
  ];
365
372
  async function stepCaveman(extDir, options) {
366
373
  if (!options.quiet)
@@ -373,7 +380,14 @@ async function stepCaveman(extDir, options) {
373
380
  let rule = bundled;
374
381
  if (remote && !options.dryRun) {
375
382
  try {
376
- rule = await withInteractiveSpinner(`Fetching Caveman ${name}`, () => httpsGet(remote));
383
+ const fetched = await withInteractiveSpinner(`Fetching Caveman ${name}`, () => httpsGet(remote));
384
+ const problem = ruleBodyProblem(fetched);
385
+ if (problem && bundled !== null) {
386
+ sayTagged(` [warn] ${name} fetch rejected (${problem}) — keeping the bundled body`);
387
+ rule = bundled;
388
+ }
389
+ else
390
+ rule = fetched;
377
391
  }
378
392
  catch (e) {
379
393
  if (bundled === null)
@@ -392,6 +406,9 @@ async function stepCaveman(extDir, options) {
392
406
  }
393
407
  }
394
408
  await copySources(extDir, filesUnder('caveman-session').filter(([, to]) => to.endsWith('index.ts')), 'caveman-session/index.ts', options);
409
+ // Renamed body: drop the stale copy an older install left behind.
410
+ if (!options.dryRun)
411
+ await fs.rm(path.join(cavemanDir, 'rule-wenyan.md'), { force: true });
395
412
  }
396
413
  async function stepTersioCommands(extDir, options) {
397
414
  if (!options.quiet)
@@ -413,7 +430,7 @@ const PONYTAIL_BUNDLE_FILES = [
413
430
  'skills/ponytail/SKILL.md',
414
431
  ];
415
432
  async function stepPonytailBundle(treeDir, options) {
416
- const root = findHoistedPackage('@dietrichgebert/ponytail', path.dirname(fileURLToPath(import.meta.url)));
433
+ const root = findHoistedPackage('@dietrichgebert/ponytail', findPackageRoot(path.dirname(fileURLToPath(import.meta.url))));
417
434
  if (!root) {
418
435
  if (!options.quiet)
419
436
  console.log(' [skip] Ponytail package not found next to the CLI — OpenCode keeps the built-in text');
@@ -441,6 +458,7 @@ async function stepOpencode(options) {
441
458
  console.log(` OpenCode — write the plugin tree (${dir})`);
442
459
  await stepSharedSessionState(dir, options);
443
460
  await copySources(dir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
461
+ await stepRtkFilter(dir, options);
444
462
  await copySources(dir, filesUnder('caveman-session'), 'caveman-session/index.ts', options);
445
463
  await stepCombo(dir, options);
446
464
  await copySources(dir, filesUnder('tersio-commands'), 'tersio-commands/index.ts', options);
@@ -474,7 +492,7 @@ async function stepOpencode(options) {
474
492
  async function resolveProfile(opts = {}) {
475
493
  // Seed from the stored defaults so flag-less runs keep them.
476
494
  const profile = await storedProfile();
477
- // 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.
495
+ // The chosen preset owns all three modes.
478
496
  let comboChosen = comboDefaultFlag !== undefined;
479
497
  if (tty() && !profileFlagsGiven && !applyUpdate) {
480
498
  const choice = await askInteractiveChoice('Session-start defaults — Combo preset', [
@@ -650,6 +668,7 @@ async function stepPiLayer(options) {
650
668
  await stepSharedSessionState(extDir, options);
651
669
  // OMP loads rtk-session from its plugin manifest, so only the pi tree copies it.
652
670
  await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
671
+ await stepRtkFilter(extDir, options);
653
672
  await stepCaveman(extDir, options);
654
673
  await stepCombo(extDir, options);
655
674
  await stepTersioCommands(extDir, options);
@@ -661,13 +680,14 @@ const PI_PONYTAIL_FILES = [
661
680
  'pi-extension/index.js',
662
681
  'hooks/ponytail-instructions.js',
663
682
  'hooks/ponytail-config.js',
664
- 'skills/ponytail/SKILL.md',
683
+ ...['ponytail', 'ponytail-audit', 'ponytail-debt', 'ponytail-gain', 'ponytail-help', 'ponytail-review']
684
+ .map((skill) => `skills/${skill}/SKILL.md`),
665
685
  ];
666
686
  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`;
667
687
  async function stepPonytailForPi(agentDir, options) {
668
688
  if (!options.quiet)
669
689
  console.log(' Ponytail — write the local pi package');
670
- const root = findHoistedPackage('@dietrichgebert/ponytail', path.dirname(fileURLToPath(import.meta.url)));
690
+ const root = findHoistedPackage('@dietrichgebert/ponytail', findPackageRoot(path.dirname(fileURLToPath(import.meta.url))));
671
691
  const dest = path.join(agentDir, 'ponytail');
672
692
  if (!root) {
673
693
  console.log(' [skip] Ponytail package not found next to the CLI');
@@ -688,14 +708,30 @@ async function stepPonytailForPi(agentDir, options) {
688
708
  }
689
709
  if (options.dryRun)
690
710
  return;
711
+ // Only when pi still declares the npm spec: with nothing to remove, `pi remove` still
712
+ // shells out to npm, which warns on stderr and exits non-zero.
713
+ const declared = await piDeclaredPackages(agentDir);
714
+ if (declared.some(isNpmPonytailPackage)) {
715
+ try {
716
+ await execNetwork('Removing the pi npm copy of Ponytail', 'pi', ['remove', 'npm:@dietrichgebert/ponytail'], { timeout: 120000 });
717
+ }
718
+ catch (e) {
719
+ sayTagged(` [warn] pi remove ponytail: ${shortError(e)}`);
720
+ }
721
+ }
722
+ await registerPiLocalPonytail(agentDir, dest, options);
723
+ }
724
+ // pi declares its package sources in settings.json; [] when the file is absent or unreadable.
725
+ async function piDeclaredPackages(agentDir) {
691
726
  try {
692
- await execNetwork('Removing the pi npm copy of Ponytail', 'pi', ['remove', 'npm:@dietrichgebert/ponytail'], { timeout: 120000 });
727
+ const settings = JSON.parse(await fs.readFile(path.join(agentDir, 'settings.json'), 'utf8'));
728
+ return Array.isArray(settings.packages) ? settings.packages : [];
693
729
  }
694
- catch (e) {
695
- sayTagged(` [warn] pi remove ponytail: ${shortError(e)}`);
730
+ catch {
731
+ return [];
696
732
  }
697
- await registerPiLocalPonytail(agentDir, dest, options);
698
733
  }
734
+ const isNpmPonytailPackage = (value) => typeof value === 'string' && value.startsWith('npm:@dietrichgebert/ponytail');
699
735
  // pi lists package sources in settings; swap the npm spec for the local dir.
700
736
  async function registerPiLocalPonytail(agentDir, dest, options) {
701
737
  const settingsPath = path.join(agentDir, 'settings.json');
@@ -707,7 +743,7 @@ async function registerPiLocalPonytail(agentDir, dest, options) {
707
743
  return;
708
744
  }
709
745
  const packages = (Array.isArray(settings.packages) ? settings.packages : []);
710
- const kept = packages.filter((p) => typeof p !== 'string' || !p.startsWith('npm:@dietrichgebert/ponytail'));
746
+ const kept = packages.filter((p) => !isNpmPonytailPackage(p));
711
747
  if (!kept.includes(dest))
712
748
  kept.push(dest);
713
749
  settings.packages = kept;
@@ -715,6 +751,10 @@ async function registerPiLocalPonytail(agentDir, dest, options) {
715
751
  if (!options.quiet)
716
752
  sayTagged(` [write] pi package source → ${dest}`);
717
753
  }
754
+ // Name the preset this run actually chose, instead of a hardcoded level.
755
+ function comboHint(host, comboDefault) {
756
+ return comboDefault === 'off' ? `\nDone — restart ${host}.` : `\nDone — restart ${host}, then /combo ${comboDefault}.`;
757
+ }
718
758
  async function runInstall() {
719
759
  // Without this guard a picked command re-enters here with command === null and loops straight back into the picker.
720
760
  if (command === null && tty() && !yes && !updatePromptDone) {
@@ -818,7 +858,7 @@ async function runInstall() {
818
858
  if (failures.length > 0)
819
859
  console.log(`\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
820
860
  else
821
- console.log('\nDone — restart OpenCode, then /combo balanced.');
861
+ console.log(comboHint('OpenCode', profile.comboDefault));
822
862
  closeRL();
823
863
  return;
824
864
  }
@@ -829,7 +869,7 @@ async function runInstall() {
829
869
  if (failures.length > 0)
830
870
  console.log(`\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
831
871
  else
832
- console.log('\nDone — restart pi, then /combo medium.');
872
+ console.log(comboHint('pi', profile.comboDefault));
833
873
  closeRL();
834
874
  return;
835
875
  }
@@ -839,6 +879,7 @@ async function runInstall() {
839
879
  await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
840
880
  await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
841
881
  await capture('rtk session', () => stepRtkSession(userExtDir, installOptions));
882
+ await capture('rtk filter', () => stepRtkFilter(userExtDir, installOptions));
842
883
  await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'), installOptions));
843
884
  await capture('combo', () => stepCombo(userExtDir, installOptions));
844
885
  await capture('commands', () => stepTersioCommands(userExtDir, installOptions));
@@ -850,7 +891,7 @@ async function runInstall() {
850
891
  console.log(` add-ons: ${failures.length} failed (${failures.join(', ')}) — see [fail] lines above`);
851
892
  }
852
893
  else {
853
- console.log(failures.length === 0 ? '\nDone — restart OMP, then /combo medium.' : `\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
894
+ console.log(failures.length === 0 ? comboHint('OMP', profile.comboDefault) : `\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
854
895
  }
855
896
  closeRL();
856
897
  }
@@ -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.
@@ -21,8 +22,9 @@ export const TREE_FILES = [
21
22
  'caveman-session/index.ts',
22
23
  'caveman-session/rule.md',
23
24
  'caveman-session/rule-ultra.md',
24
- 'caveman-session/rule-wenyan.md',
25
+ 'caveman-session/rule-megacave.md',
25
26
  'rtk-session/index.ts',
27
+ 'rtk-filter/index.ts',
26
28
  'combo-toggle/index.ts',
27
29
  'tersio-commands/index.ts',
28
30
  'ai-addons-updater/index.ts',
@@ -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();
@@ -290,7 +290,7 @@ async function runUninstall(options = {}) {
290
290
  }
291
291
  // Cleared last: writeConfigLines above would just make a fresh one.
292
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-wenyan.md.bak')]) {
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')]) {
294
294
  await removeUninstallTarget(backup, shouldDryRun, false);
295
295
  }
296
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_ULTRA, CAVEMAN_REMOTE_WENYAN, 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) {
@@ -84,9 +84,9 @@ async function probeUpdatePlan(cliLatest) {
84
84
  const [rtkLocal, rtkRelease, ruleLocal, ruleRemote, ponytailLocalText] = await Promise.all([
85
85
  settle(rtkBin ? execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null) : Promise.resolve(null), 6000),
86
86
  settle(httpsGet(RTK_RELEASE_API, { signal: controller.signal }).then((text) => normalizeRtkVersion(JSON.parse(text).tag_name) || null), 1500),
87
- Promise.all([readTextIfExists(tersioRule('rule-ultra.md')), readTextIfExists(tersioRule('rule-wenyan.md'))])
87
+ Promise.all([readTextIfExists(tersioRule('rule-ultra.md')), readTextIfExists(tersioRule('rule-megacave.md'))])
88
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_WENYAN, { signal: controller.signal })])
89
+ settle(Promise.all([httpsGet(CAVEMAN_REMOTE_ULTRA, { signal: controller.signal }), httpsGet(CAVEMAN_REMOTE_MEGACAVE, { signal: controller.signal })])
90
90
  .then((texts) => texts.map((t) => sha256Hex(t).slice(0, 8)).join('+')), 1500),
91
91
  readTextIfExists(ponytailPkg),
92
92
  ]);
@@ -214,8 +214,7 @@ async function runLatestUpdate() {
214
214
  // Inherited stdio, no outer spinner; the delegated installer runs quiet and this parent owns both the plan line and the closing summary.
215
215
  try {
216
216
  await execInherit(npmCommand, npmCommandArgs);
217
- const hostName = pinned === 'pi' ? 'pi' : pinned === 'opencode' ? 'OpenCode' : 'OMP';
218
- console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart ${hostName}.`);
217
+ console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION} and add-ons updated.`);
219
218
  if (!dryRun) {
220
219
  try {
221
220
  await refreshPrices();
@@ -6,7 +6,7 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { setExtensionLabel } from '../shared/host.js';
9
- import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_WENYAN, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, isPiProcess, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../lib/utils.js';
9
+ import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../lib/utils.js';
10
10
  const IS_WINDOWS = process.platform === 'win32';
11
11
  const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
12
12
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
@@ -16,10 +16,10 @@ const cavemanRule = (name) => path.join(EXTENSION_DIR, '..', 'caveman-session',
16
16
  const CAVEMAN_LOCAL = cavemanRule('rule.md');
17
17
  const CAVEMAN_TRACKED = [
18
18
  { file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
19
- { file: cavemanRule('rule-wenyan.md'), remote: CAVEMAN_REMOTE_WENYAN },
19
+ { file: cavemanRule('rule-megacave.md'), remote: CAVEMAN_REMOTE_MEGACAVE },
20
20
  ];
21
- const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
22
- const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
21
+ // Host-neutral: this updater runs inside pi, OMP, and OpenCode alike.
22
+ const RELOAD_MSG = 'Reminder: restart your agent (or reload extensions) for updates to take effect.';
23
23
  // Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
24
24
  function ponytailLocal() {
25
25
  return findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'package.json');
@@ -7,9 +7,14 @@ import { announceStatus } from '../shared/status.js';
7
7
  import { readCavemanDefault } from '../shared/plugin-settings.js';
8
8
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
9
9
  // Upstream ships one skill per register: default, ultracave, megacave.
10
- const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', 'wenyan-lite': 'rule-wenyan.md', 'wenyan-full': 'rule-wenyan.md', 'wenyan-ultra': 'rule-wenyan.md' };
10
+ const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave.md' };
11
11
  const DEFAULT_MODE = 'off';
12
+ // A rule file is immutable for the process, so read each level once; reinstalling needs a restart.
13
+ const instructionCache = new Map();
12
14
  function buildInstruction(mode) {
15
+ const cached = instructionCache.get(mode);
16
+ if (cached)
17
+ return cached;
13
18
  const name = RULE_FILES[mode] ?? RULE_FILES.full;
14
19
  // Fail loud: a wrong rule reads to the model as the real one.
15
20
  let body;
@@ -17,9 +22,11 @@ function buildInstruction(mode) {
17
22
  body = readFileSync(join(CAVERN_DIR, name), 'utf8');
18
23
  }
19
24
  catch {
20
- body = `Caveman ${mode} requested, but ${name} is missing. Run: tersio doctor --fix extensions`;
25
+ return `Caveman ${mode} requested, but ${name} is missing. Run: tersio doctor --fix extensions`;
21
26
  }
22
- return `Caveman ${mode} active for this session.\n${body}`;
27
+ const instruction = `Caveman ${mode} active for this session.\n${body}`;
28
+ instructionCache.set(mode, instruction);
29
+ return instruction;
23
30
  }
24
31
  function resolveMode(entries, fallback = DEFAULT_MODE) {
25
32
  return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
@@ -62,7 +69,7 @@ export default function cavemanSessionExtension(pi) {
62
69
  return;
63
70
  }
64
71
  if (!setMode(arg, ctx)) {
65
- ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status]', 'warning');
72
+ ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|megacave-lite|megacave-full|megacave-ultra|off|status]', 'warning');
66
73
  }
67
74
  },
68
75
  });
@@ -110,6 +117,6 @@ export default function cavemanSessionExtension(pi) {
110
117
  // The previous level's block is replaced, not stacked on.
111
118
  const stale = lastInjected;
112
119
  lastInjected = instruction;
113
- return injectPromptText(pi, event, instruction, stale);
120
+ return injectPromptText(pi, event, { text: instruction, staleMarker: stale, sectionTag: 'caveman' });
114
121
  });
115
122
  }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: ultracave
3
+ description: >
4
+ Caveman at maximum compression: fragments, one word when one word is enough,
5
+ each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until
6
+ "stop caveman" or "normal mode".
7
+ disable-model-invocation: true
8
+ ---
9
+
10
+ # ultracave
11
+
12
+ Respond terse like smart caveman. All technical substance stay. Only fluff die. Then cut again.
13
+
14
+ Ultracave is caveman with the grammar stripped. Payload only.
15
+
16
+ ## Persistence
17
+
18
+ Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value or say `Caveman mode: unknown`.
19
+
20
+ ## Floor
21
+
22
+ Never cut: code, commands, paths, API names, error strings (verbatim). not/never/no/only/except. Numbers and units. The user's language. One term per thing. No invented abbreviations, no arrows.
23
+
24
+ ## Rules
25
+
26
+ ### 1. Fragments
27
+
28
+ Drop articles, copulas, connectives when order stays clear.
29
+
30
+ Bad: "The component re-renders because an inline object prop creates a new reference each render."
31
+ Good: "Inline object prop, new ref, re-render. `useMemo`."
32
+
33
+ ### 2. One word when one word is enough
34
+
35
+ Bad: "Yes, that should work, though you'll want a null check first."
36
+ Good: "Yes. Null check first."
37
+
38
+ ### 3. Each fact once
39
+
40
+ No restating, no summary after a list.
41
+
42
+ Good: "Pool reuses open DB connections. No per-request handshake."
43
+
44
+ ### 4. Cut conjunctions only when order survives
45
+
46
+ Bad: "Migrate table drop column backup first."
47
+ Good: "Back up first. Then migrate: drops column."
48
+
49
+ ### 5. Tool runs
50
+
51
+ One line in, one line out. Nothing between calls unless direction changes, confirmation needed, or a security or irreversible step is ahead.
52
+
53
+ ### 6. Never perform
54
+
55
+ No prefix, no announcement, no mangled verbs for flavor. Fragment not shorter than the sentence? Use the sentence.
56
+
57
+ ## When to break the rules
58
+
59
+ Plain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
60
+
61
+ ## Pre-send check
62
+
63
+ Two readings? Full sentence. Negations present? Payload verbatim? Flavor word? Cut.