@krtclcdy/tersio 2.25.4 → 2.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +27 -63
  2. package/dashboard/dist/index.html +18 -16
  3. package/dist/cli/backups.js +187 -0
  4. package/dist/cli/banner.js +4 -3
  5. package/dist/cli/common.js +29 -13
  6. package/dist/cli/currency.js +1 -1
  7. package/dist/cli/dashboard.js +168 -110
  8. package/dist/cli/doctor-fix.js +232 -50
  9. package/dist/cli/doctor.js +281 -32
  10. package/dist/cli/hosts.js +29 -9
  11. package/dist/cli/install.js +204 -69
  12. package/dist/cli/interactive.js +3 -3
  13. package/dist/cli/manifest.js +125 -17
  14. package/dist/cli/profile.js +14 -9
  15. package/dist/cli/rtk-wiring.js +1 -1
  16. package/dist/cli/settings.js +20 -5
  17. package/dist/cli/uninstall.js +24 -8
  18. package/dist/cli/update.js +9 -10
  19. package/dist/cli/usage.js +75 -7
  20. package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
  21. package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
  22. package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
  23. package/dist/extensions/caveman/rule.md +89 -0
  24. package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
  25. package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
  26. package/dist/extensions/pstack/index.js +119 -0
  27. package/dist/extensions/pstack/rule.md +65 -0
  28. package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
  29. package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
  30. package/dist/extensions/{opencode/server.js → server.js} +98 -14
  31. package/dist/extensions/shared/agent-stats.js +1198 -0
  32. package/dist/extensions/shared/brand-marks.js +153 -0
  33. package/dist/extensions/shared/carbon.js +1 -1
  34. package/dist/extensions/shared/plugin-settings.js +12 -7
  35. package/dist/extensions/shared/pricing.js +86 -30
  36. package/dist/extensions/shared/rtk-gain.js +4 -9
  37. package/dist/extensions/shared/session-state.js +15 -10
  38. package/dist/extensions/shared/sqlite.js +168 -0
  39. package/dist/extensions/shared/status.js +8 -8
  40. package/dist/extensions/shared/types.js +1 -1
  41. package/dist/extensions/shared/usage-ledger.js +113 -39
  42. package/dist/extensions/shared/usage-store.js +191 -48
  43. package/dist/extensions/{lib → shared}/utils.js +6 -6
  44. package/dist/extensions/timing/index.js +31 -0
  45. package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
  46. package/dist/probe-catalog.js +48 -0
  47. package/dist/probe-catalog188.js +44 -0
  48. package/dist/tersio.js +9 -4
  49. package/extensions/{caveman-session → caveman}/index.ts +4 -2
  50. package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
  51. package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
  52. package/extensions/caveman/rule.md +89 -0
  53. package/extensions/{combo-toggle → combo}/index.ts +62 -19
  54. package/extensions/{tersio-commands → commands}/index.ts +2 -2
  55. package/extensions/pstack/index.ts +128 -0
  56. package/extensions/pstack/rule.md +65 -0
  57. package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
  58. package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
  59. package/extensions/{opencode/server.ts → server.ts} +81 -16
  60. package/extensions/shared/agent-stats.ts +1605 -0
  61. package/extensions/shared/brand-marks.ts +163 -0
  62. package/extensions/shared/carbon.ts +1 -1
  63. package/extensions/shared/plugin-settings.ts +13 -7
  64. package/extensions/shared/pricing.ts +95 -32
  65. package/extensions/shared/rtk-gain.ts +4 -9
  66. package/extensions/shared/session-state.ts +18 -13
  67. package/extensions/shared/sqlite.ts +168 -0
  68. package/extensions/shared/status.ts +9 -8
  69. package/extensions/shared/types.ts +2 -1
  70. package/extensions/shared/usage-ledger.ts +134 -46
  71. package/extensions/shared/usage-store.ts +202 -50
  72. package/extensions/{lib → shared}/utils.ts +6 -6
  73. package/extensions/timing/index.ts +45 -0
  74. package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
  75. package/package.json +27 -18
  76. package/dist/extensions/caveman-session/rule.md +0 -79
  77. package/extensions/caveman-session/rule.md +0 -79
@@ -1,13 +1,10 @@
1
1
  // Single list of install writes, shared by installer, doctor, and tests.
2
2
  import path from 'node:path';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
- import { findPackageRoot } from '../extensions/lib/utils.js';
5
+ import { findPackageRoot, readTextIfExists } from '../extensions/shared/utils.js';
6
+ import { writeIfChanged } from './common.js';
5
7
  export const EXT_DIR = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'extensions');
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). */
7
- const SERVER_SPEC = `./${['opencode', 'server.ts'].join('/')}`;
8
- export const OPENCODE_SERVER_SHIM = `// Generated by \`tersio install --host opencode\`: OpenCode resolves <pluginDir>/server.ts.
9
- export { default } from "${SERVER_SPEC}";
10
- `;
11
8
  /** Every file in the tree, as paths relative to the extensions directory. */
12
9
  export const TREE_FILES = [
13
10
  'shared/host.ts',
@@ -16,19 +13,23 @@ export const TREE_FILES = [
16
13
  'shared/types.ts',
17
14
  'shared/plugin-settings.ts',
18
15
  'shared/usage-ledger.ts',
16
+ 'shared/sqlite.ts',
19
17
  'shared/pricing.ts',
20
18
  'shared/carbon.ts',
21
- 'lib/utils.ts',
22
- 'caveman-session/index.ts',
23
- 'caveman-session/rule.md',
24
- 'caveman-session/rule-ultra.md',
25
- 'caveman-session/rule-megacave.md',
26
- 'rtk-session/index.ts',
27
- 'rtk-filter/index.ts',
28
- 'combo-toggle/index.ts',
29
- 'tersio-commands/index.ts',
30
- 'ai-addons-updater/index.ts',
31
- 'opencode/server.ts',
19
+ 'shared/utils.ts',
20
+ 'caveman/index.ts',
21
+ 'caveman/rule.md',
22
+ 'caveman/rule-ultra.md',
23
+ 'caveman/rule-megacave.md',
24
+ 'pstack/index.ts',
25
+ 'pstack/rule.md',
26
+ 'rtk/session.ts',
27
+ 'rtk/filter.ts',
28
+ 'timing/index.ts',
29
+ 'combo/index.ts',
30
+ 'commands/index.ts',
31
+ 'updater/index.ts',
32
+ 'server.ts',
32
33
  ];
33
34
  export function sourcePath(relative) {
34
35
  return path.join(EXT_DIR, ...relative.split('/'));
@@ -42,3 +43,110 @@ export function filesUnder(dir) {
42
43
  }
43
44
  return found;
44
45
  }
46
+ /** The entries at the extensions root, which `filesUnder` cannot name because it takes a directory. */
47
+ export function rootFiles() {
48
+ return TREE_FILES.filter((file) => !file.includes('/')).map((file) => [sourcePath(file), file]);
49
+ }
50
+ /** The OpenCode plugin entry: imports `@opencode/plugin`, absent from omp and pi loaders, so it ships there alone. */
51
+ export const OPENCODE_ENTRY = 'server.ts';
52
+ /** pi's rtk entry manifest, generated rather than copied: no other host loads rtk that way. */
53
+ export const PI_RTK_MANIFEST = 'rtk/package.json';
54
+ /** pi's rtk entry manifest, generated rather than copied: no other host loads rtk that way. */
55
+ export const PI_RTK_MANIFEST_BODY = `${JSON.stringify({ pi: { extensions: ['./session.ts', './filter.ts'] } }, null, 2)}\n`;
56
+ /** Files the install generates, keyed by tree path, so one repair path handles copied and generated alike. */
57
+ export const GENERATED_TREE_FILES = {
58
+ [PI_RTK_MANIFEST]: PI_RTK_MANIFEST_BODY,
59
+ };
60
+ /** The files one host's extension tree holds. The package tree holds all of them. */
61
+ export function treeFilesFor(id) {
62
+ if (id === 'opencode')
63
+ return TREE_FILES;
64
+ const files = TREE_FILES.filter((file) => file !== OPENCODE_ENTRY);
65
+ return id === 'pi' ? [...files, PI_RTK_MANIFEST] : files;
66
+ }
67
+ /** Obsolete install debris; explicit, not a sweep, because these trees are shared with tersio's other steps. */
68
+ const OBSOLETE_DIRS = ['caveman-session', 'rtk-filter', 'rtk-session', 'combo-toggle', 'tersio-commands', 'ai-addons-updater', 'lib', 'opencode'];
69
+ /** Debris test for a tree-relative path; one definition shared by detection and repair. */
70
+ export function isObsoleteTreeFile(rel, id) {
71
+ if (rel.endsWith('.bak') || rel === '.DS_Store')
72
+ return true;
73
+ if (OBSOLETE_DIRS.includes(rel.split('/')[0]))
74
+ return true;
75
+ return rel === 'server.ts' && id !== 'opencode';
76
+ }
77
+ /** A tree file's text: copied files come from the repo, generated ones from their own definition. */
78
+ export function treeFileText(file) {
79
+ const generated = GENERATED_TREE_FILES[file];
80
+ if (generated !== undefined)
81
+ return generated;
82
+ try {
83
+ return readFileSync(sourcePath(file), 'utf8');
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /** The ponytail ruleset this build ships, copied to every host that cannot install the package. */
90
+ export const PONYTAIL_SKILL_NAMES = ['ponytail', 'ponytail-audit', 'ponytail-debt', 'ponytail-gain', 'ponytail-help', 'ponytail-review'];
91
+ /** The OpenCode bundle: no node_modules there, so the ruleset rides along as files. */
92
+ export const PONYTAIL_BUNDLE_FILES = [
93
+ 'hooks/ponytail-instructions.js',
94
+ 'hooks/ponytail-config.js',
95
+ ...PONYTAIL_SKILL_NAMES.map((skill) => `skills/${skill}/SKILL.md`),
96
+ ];
97
+ /** pi's local package: the same ruleset, plus the extension that injects it. */
98
+ export const PI_PONYTAIL_FILES = ['pi-extension/index.js', ...PONYTAIL_BUNDLE_FILES];
99
+ /** The shipped package manifest, one level above the extension tree. */
100
+ const MANIFEST_PATH = path.join(path.dirname(EXT_DIR), 'package.json');
101
+ /** The `omp` block a host reads for what to load: extensions, features, and the settings schema. */
102
+ export function shippedOmpManifest() {
103
+ try {
104
+ const pkg = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
105
+ return typeof pkg.omp === 'object' && pkg.omp !== null ? pkg.omp : null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /**
112
+ * Write the shipped `omp` block over an installed plugin's manifest. The tree files come from this
113
+ * build, so an entry added here reaches the host only when its manifest names it.
114
+ */
115
+ export async function syncPluginManifest(pluginDir, options = {}) {
116
+ const omp = shippedOmpManifest();
117
+ if (omp === null)
118
+ return;
119
+ const pkgPath = path.join(pluginDir, 'package.json');
120
+ let pkg;
121
+ try {
122
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
123
+ }
124
+ catch {
125
+ return;
126
+ }
127
+ if (JSON.stringify(pkg.omp) === JSON.stringify(omp))
128
+ return;
129
+ pkg.omp = omp;
130
+ await writeIfChanged(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, options);
131
+ }
132
+ /** Copy bundled files into `dest`; misses are reported and skipped, `skip` keeps newer fetched rule bodies. */
133
+ export async function syncTreeFiles(dest, files, options, skip) {
134
+ const missing = [];
135
+ for (const file of files) {
136
+ if (skip?.(file))
137
+ continue;
138
+ const generated = GENERATED_TREE_FILES[file];
139
+ if (generated !== undefined) {
140
+ await writeIfChanged(path.join(dest, ...file.split('/')), generated, options);
141
+ continue;
142
+ }
143
+ const from = sourcePath(file);
144
+ const text = await readTextIfExists(from);
145
+ if (text === null) {
146
+ missing.push(from);
147
+ continue;
148
+ }
149
+ await writeIfChanged(path.join(dest, ...file.split('/')), text, options);
150
+ }
151
+ return missing;
152
+ }
@@ -1,10 +1,10 @@
1
1
  // cli/profile.ts — session-start defaults and the display-currency default,
2
2
  import { existsSync, promises as fs, readFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
4
+ import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, verbose, } from './common.js';
5
5
  import os from 'node:os';
6
6
  import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
7
- import { readTextIfExists } from '../extensions/lib/utils.js';
7
+ import { readTextIfExists } from '../extensions/shared/utils.js';
8
8
  import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
9
9
  function readTextIfExistsSync(file) {
10
10
  try {
@@ -21,12 +21,13 @@ function defaultProfile() {
21
21
  cavemanDefault: 'off',
22
22
  rtkDefault: false,
23
23
  ponytailDefault: 'off',
24
+ pstackDefault: 'off',
24
25
  currency: DEFAULT_CURRENCY,
25
26
  backupSchedule: 'monthly',
26
27
  subagentMarkers: [],
27
28
  };
28
29
  }
29
- // Seed once from OMP plugin settings so a pre-~/.tersio install keeps its values. Sync: awaiting let piped stdin arrive before `ask()` attached, so a scripted answer was swallowed and a destructive confirm defaulted to abort.
30
+ // Seed from OMP settings so pre-~/.tersio installs keep values. Sync: awaiting let piped stdin beat `ask()`.
30
31
  function storedProfileSync() {
31
32
  const base = defaultProfile();
32
33
  const stored = parseStored(readTextIfExistsSync(tersioSettingsFile()) ?? readTextIfExistsSync(legacyOmpLockPath()));
@@ -44,6 +45,8 @@ function applyStored(base, stored) {
44
45
  base.rtkDefault = stored.rtkDefault;
45
46
  if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
46
47
  base.ponytailDefault = stored.ponytailDefault;
48
+ if (typeof stored.pstackDefault === 'string' && PSTACK_DEFAULTS.has(stored.pstackDefault))
49
+ base.pstackDefault = stored.pstackDefault;
47
50
  if (typeof stored.currency === 'string' && isCurrencyCode(stored.currency.trim().toUpperCase())) {
48
51
  base.currency = stored.currency.trim().toUpperCase();
49
52
  }
@@ -80,7 +83,7 @@ function parseStored(raw) {
80
83
  return null;
81
84
  }
82
85
  }
83
- // Persist the profile where every host reads it. `omp plugin config get` no longer reports it; `tersio settings` does.
86
+ // Persist profile where every host reads it. `omp plugin config get` no longer reports it; `tersio settings` does.
84
87
  async function writePluginSettings(profile, options) {
85
88
  const file = tersioSettingsFile();
86
89
  const values = {
@@ -90,6 +93,7 @@ async function writePluginSettings(profile, options) {
90
93
  cavemanDefault: profile.cavemanDefault,
91
94
  rtkDefault: profile.rtkDefault,
92
95
  ponytailDefault: profile.ponytailDefault,
96
+ pstackDefault: profile.pstackDefault,
93
97
  currency: profile.currency,
94
98
  backupSchedule: profile.backupSchedule,
95
99
  ...(profile.subagentMarkers.length > 0 ? { subagentMarkers: profile.subagentMarkers } : {}),
@@ -104,21 +108,22 @@ async function writePluginSettings(profile, options) {
104
108
  console.log(` [write] Session defaults in ${file}`);
105
109
  }
106
110
  function formatProfile(profile) {
107
- return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault}) · currency=${profile.currency}`;
111
+ return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault} · pstack=${profile.pstackDefault}) · currency=${profile.currency}`;
108
112
  }
109
113
  export { defaultProfile, formatProfile, storedProfile, storedProfileSync, writePluginSettings };
110
114
  /** The status line the extensions print, read from the persisted profile. */
111
115
  export function formatCliStatus(profile) {
112
- const { comboDefault, cavemanDefault, rtkDefault, ponytailDefault } = profile;
113
- if (comboDefault === 'off' && cavemanDefault === 'off' && !rtkDefault && ponytailDefault === 'off') {
116
+ const { comboDefault, cavemanDefault, rtkDefault, ponytailDefault, pstackDefault } = profile;
117
+ if (comboDefault === 'off' && cavemanDefault === 'off' && !rtkDefault && ponytailDefault === 'off' && pstackDefault === 'off') {
114
118
  return '🧩 combo OFF';
115
119
  }
116
120
  const preset = COMBO_PRESET_MODES[comboDefault];
117
121
  const isPreset = preset !== undefined
118
122
  && preset.caveman === cavemanDefault
119
123
  && preset.rtk === rtkDefault
120
- && preset.ponytail === ponytailDefault;
124
+ && preset.ponytail === ponytailDefault
125
+ && preset.pstack === pstackDefault;
121
126
  const level = isPreset ? comboDefault.toUpperCase() : 'CUSTOM';
122
127
  const rtk = rtkDefault ? 'ON' : 'OFF';
123
- return `🧩 combo ${level}: 🪨caveman=${cavemanDefault.toUpperCase()} ⚡rtk=${rtk} 🦥ponytail=${ponytailDefault.toUpperCase()}`;
128
+ return `🧩 combo ${level}: 🪨caveman=${cavemanDefault.toUpperCase()} ⚡rtk=${rtk} 🦥ponytail=${ponytailDefault.toUpperCase()} 🥔pstack=${pstackDefault.toUpperCase()}`;
124
129
  }
@@ -61,7 +61,7 @@ function rtkExtensionPath() {
61
61
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
62
62
  return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
63
63
  }
64
- // Append rtk.ts after the existing entries (or create the key) so the wire takes effect on the next OMP start.
64
+ // Append rtk.ts so the wire takes effect on the next OMP start.
65
65
  export async function ensureRtkInConfig(options) {
66
66
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
67
67
  const configPath = path.join(home, '.omp', 'agent', 'config.yml');
@@ -1,5 +1,5 @@
1
1
  // cli/settings.ts — view/edit session-start defaults and the display currency.
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';
2
+ import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, subagentMarkerFlags, subagentMarkersGiven, } from './common.js';
3
3
  import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
4
4
  import { CURRENCY_CODES } from './currency.js';
5
5
  import { textTable } from './usage.js';
@@ -14,12 +14,15 @@ function applyFlags(base) {
14
14
  next.cavemanDefault = preset.caveman;
15
15
  next.rtkDefault = preset.rtk;
16
16
  next.ponytailDefault = preset.ponytail;
17
+ next.pstackDefault = preset.pstack;
17
18
  if (cavemanDefaultFlag !== undefined)
18
19
  next.cavemanDefault = cavemanDefaultFlag;
19
20
  if (rtkDefaultFlag !== undefined)
20
21
  next.rtkDefault = rtkDefaultFlag === 'on';
21
22
  if (ponytailDefaultFlag !== undefined)
22
23
  next.ponytailDefault = ponytailDefaultFlag;
24
+ if (pstackDefaultFlag !== undefined)
25
+ next.pstackDefault = pstackDefaultFlag;
23
26
  if (subagentMarkersGiven)
24
27
  next.subagentMarkers = subagentMarkerFlags;
25
28
  if (currencyGiven)
@@ -41,6 +44,7 @@ async function askProfile(current) {
41
44
  cavemanDefault: preset.caveman,
42
45
  rtkDefault: preset.rtk,
43
46
  ponytailDefault: preset.ponytail,
47
+ pstackDefault: preset.pstack,
44
48
  currency: current.currency,
45
49
  backupSchedule: current.backupSchedule,
46
50
  subagentMarkers: current.subagentMarkers,
@@ -82,11 +86,11 @@ async function askDiagSchedule(current) {
82
86
  return null;
83
87
  return picked.value;
84
88
  }
85
- const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis', 'markers'];
89
+ const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'pstack', 'currency', 'diagnosis', 'markers'];
86
90
  function settingsUsage() {
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]');
91
+ console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|pstack|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] [--pstack-default on|off] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--subagent-marker <text>]... [--dry-run]');
88
92
  }
89
- // Single-setting jump: `tersio settings diagnosis` prompts only that value instead of walking the whole chain. Returns false when aborted.
93
+ // Single-setting jump: prompts only that value instead of walking the whole chain; null when aborted.
90
94
  async function runSingleSetting(name, current, nextDiag) {
91
95
  const next = { ...current };
92
96
  let diag = nextDiag;
@@ -105,6 +109,7 @@ async function runSingleSetting(name, current, nextDiag) {
105
109
  next.cavemanDefault = preset.caveman;
106
110
  next.rtkDefault = preset.rtk;
107
111
  next.ponytailDefault = preset.ponytail;
112
+ next.pstackDefault = preset.pstack;
108
113
  break;
109
114
  }
110
115
  case 'caveman': {
@@ -135,6 +140,15 @@ async function runSingleSetting(name, current, nextDiag) {
135
140
  next.ponytailDefault = picked.value;
136
141
  break;
137
142
  }
143
+ case 'pstack': {
144
+ const picked = await askInteractiveChoice('pstack default (rigor mode)', [...PSTACK_DEFAULTS].map((v) => ({
145
+ value: v, label: v,
146
+ })), next.pstackDefault);
147
+ if (picked.status !== 'selected')
148
+ return null;
149
+ next.pstackDefault = picked.value;
150
+ break;
151
+ }
138
152
  case 'currency': {
139
153
  const picked = await askInteractiveChoice('Display currency (usage/dashboard reports)', CURRENCY_CODES.map((v) => ({
140
154
  value: v, label: v,
@@ -173,6 +187,7 @@ function printSettingsTable(current) {
173
187
  ['caveman', current.cavemanDefault, [...CAVEMAN_DEFAULTS].join(' | ')],
174
188
  ['rtk', current.rtkDefault ? 'on' : 'off', 'on | off'],
175
189
  ['ponytail', current.ponytailDefault, [...PONYTAIL_DEFAULTS].join(' | ')],
190
+ ['pstack', current.pstackDefault, [...PSTACK_DEFAULTS].join(' | ')],
176
191
  ['diagnosis', readDiagSchedule(), 'manual | daily | weekly | monthly'],
177
192
  ['markers', current.subagentMarkers.length === 0 ? 'default' : current.subagentMarkers.join(' | '), 'host subagent prompt text'],
178
193
  ];
@@ -180,7 +195,7 @@ function printSettingsTable(current) {
180
195
  console.log(l);
181
196
  }
182
197
  console.log(` Stored: ${tersioSettingsFile()}`);
183
- console.log(` Combo ${current.comboDefault} sets caveman/rtk/ponytail. Tune one: tersio settings <name>`);
198
+ console.log(` Combo ${current.comboDefault} sets all four modes. Tune one: tersio settings <name>`);
184
199
  }
185
200
  async function runSettings() {
186
201
  const current = await storedProfile();
@@ -4,19 +4,30 @@ 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 { piAgentDir, readTextIfExists } from '../extensions/lib/utils.js';
7
+ import { piAgentDir, readTextIfExists } from '../extensions/shared/utils.js';
8
8
  import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
9
9
  // The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a shared binary are not ours to delete.
10
10
  const PONYTAIL_PKG = '@dietrichgebert/ponytail';
11
11
  const PI_TREE_DIRS = [
12
+ // What an install writes now.
13
+ 'caveman',
14
+ 'pstack',
15
+ 'rtk',
16
+ 'updater',
17
+ 'combo',
18
+ 'commands',
19
+ 'shared',
20
+ 'server.ts',
21
+ // Legacy: a tree an older build wrote, or a foreign name that was never shipped here.
12
22
  'caveman-session',
13
23
  'rtk-session',
24
+ 'rtk-filter',
14
25
  'ai-addons-updater',
15
26
  'combo-toggle',
16
27
  'tersio-commands',
17
- 'shared',
18
28
  'lib',
19
29
  'opencode',
30
+ 'aaa-combo-boot',
20
31
  ];
21
32
  // One `pi remove`, for both packages the installer adds there.
22
33
  async function piRemove(spec, shouldDryRun) {
@@ -90,7 +101,7 @@ async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRem
90
101
  await removeCopiedPonytailSkills(shouldDryRun);
91
102
  }
92
103
  await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
93
- // rtk's wiring is a loose file, not one of the tree dirs, so it survived --remove-rtk on pi while OMP removed its copy.
104
+ // rtk's wiring is a loose file, not a tree dir, so it survived --remove-rtk on pi while OMP removed its copy.
94
105
  if (shouldRemoveRtk) {
95
106
  await removeUninstallTarget(rtkWiring, shouldDryRun, false);
96
107
  await removeUninstallTarget(path.join(BUN_BIN_DIR, RTK_BINARY_NAME), shouldDryRun, false);
@@ -237,15 +248,21 @@ async function runUninstall(options = {}) {
237
248
  const pluginsDir = OMP_PLUGINS_DIR;
238
249
  const ponytailPkgDir = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
239
250
  const targets = [
251
+ // What an install writes now.
252
+ 'caveman',
253
+ 'rtk',
254
+ 'updater',
255
+ 'combo',
256
+ 'commands',
257
+ 'shared',
258
+ // Legacy: a tree an older build wrote into the agent tree, or a foreign name never shipped here.
240
259
  'caveman-session',
241
260
  'rtk-session',
261
+ 'rtk-filter',
242
262
  'ai-addons-updater',
243
263
  'combo-toggle',
244
264
  'tersio-commands',
245
- 'shared',
246
- // Only the updater reads it, and it runs before this.
247
265
  'lib',
248
- // Legacy: imports session-state.js, so it warns once that module is gone.
249
266
  'aaa-combo-boot',
250
267
  // Foreign to the OMP loader; never shipped here on purpose.
251
268
  'opencode',
@@ -265,7 +282,6 @@ async function runUninstall(options = {}) {
265
282
  return false;
266
283
  // Remove extension directories (independent paths, so concurrently).
267
284
  await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
268
- // Remove Combo, mode-reinforcement, and rtk registrations; Ponytail on request.
269
285
  const configRaw = await readTextIfExists(configPath);
270
286
  if (configRaw) {
271
287
  let lines = configRaw.split('\n');
@@ -275,7 +291,7 @@ async function runUninstall(options = {}) {
275
291
  return false;
276
292
  if (shouldRemovePonytail && l.includes('ponytail') && l.includes('pi-extension'))
277
293
  return false;
278
- // Our rtk wiring, absolute or relative. Leaving it listed would keep OMP loading a hook for a package that is no longer installed.
294
+ // Our rtk wiring, absolute or relative. Leaving it listed would keep OMP loading a hook for an uninstalled package.
279
295
  const entry = l.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '');
280
296
  if (entry.endsWith(`extensions${path.sep}rtk.ts`) || entry.endsWith('extensions/rtk.ts'))
281
297
  return false;
@@ -4,9 +4,9 @@ import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, execP, parseJsonObject, verbose, } from './common.js';
6
6
  import { execNetwork, sayTagged } from './interactive.js';
7
- import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/lib/utils.js';
7
+ import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/shared/utils.js';
8
8
  import { refreshPrices } from '../extensions/shared/pricing.js';
9
- import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, 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/shared/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) {
@@ -35,7 +35,7 @@ async function latestPublishedVersion() {
35
35
  return null;
36
36
  }
37
37
  }
38
- // A newer published version, null when up to date, 'unknown' when the registry is unreachable. Cached for a TTL unless force, which must not trust a cache.
38
+ // A newer version, null when up to date, 'unknown' when the registry is unreachable. Cached for a TTL unless force.
39
39
  async function checkForUpdate(force = false) {
40
40
  const cachePath = tersioDataPath('update-check.json', 'tersio-update-check.json');
41
41
  const cached = parseJsonObject(await readTextIfExists(cachePath));
@@ -48,7 +48,7 @@ async function checkForUpdate(force = false) {
48
48
  await fs.writeFile(cachePath, JSON.stringify({ latest, lastCheck: Date.now() }) + '\n', 'utf8').catch(() => { });
49
49
  return newerThan(latest, PACKAGE_VERSION) ? latest : null;
50
50
  }
51
- // Unreachable registry: a stale cache naming a newer release is still actionable, else report unknown so nobody claims "latest".
51
+ // Unreachable registry: a stale cache naming a newer release is actionable, else unknown, so none claim "latest".
52
52
  return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : 'unknown';
53
53
  }
54
54
  // Race a probe against a timeout; failures resolve null so nothing blocks.
@@ -74,12 +74,12 @@ async function execInherit(cmd, args) {
74
74
  if (code !== 0)
75
75
  throw new Error(`${cmd} exited with code ${code}`);
76
76
  }
77
- // Current → latest per add-on. Every probe is capped and nullable, and the plan prints `unknown` for anything unreachable.
77
+ // Current → latest per add-on. Probes are capped and nullable; the plan prints `unknown` for anything unreachable.
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
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
+ const tersioRule = (name) => path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
83
83
  const controller = new AbortController();
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),
@@ -98,7 +98,7 @@ async function probeUpdatePlan(cliLatest) {
98
98
  ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
99
99
  };
100
100
  }
101
- // Every add-on prints a version jump when stale, "up to date" otherwise, and "unknown" when its source cannot be reached.
101
+ // Every add-on prints a version jump when stale, "up to date" otherwise, and "unknown" when unreachable.
102
102
  function planLines(plan) {
103
103
  const stale = [];
104
104
  const status = [];
@@ -147,7 +147,6 @@ async function runLatestUpdate() {
147
147
  // `--prefer-online` beats the local cache, so a stale @latest cannot pin down.
148
148
  const cliLatest = await latestPublishedVersion();
149
149
  const target = cliLatest && /^\d+\.\d+\.\d+$/.test(cliLatest) ? `@${cliLatest}` : '@latest';
150
- // Best-effort: unreachable probes print as unknown.
151
150
  const plan = await probeUpdatePlan(cliLatest);
152
151
  const { stale, status } = planLines(plan);
153
152
  if (dryRun) {
@@ -181,7 +180,7 @@ async function runLatestUpdate() {
181
180
  process.exitCode = 1;
182
181
  return;
183
182
  }
184
- // The npx delegation below only refreshes the OMP-side files; the globally installed CLI keeps its old version until npm -g runs. Refresh both.
183
+ // The npx delegation below only refreshes the OMP-side files; the global CLI keeps its old version until npm -g runs.
185
184
  const globalArgs = IS_WINDOWS
186
185
  ? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online']
187
186
  : ['install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online'];
@@ -211,7 +210,7 @@ async function runLatestUpdate() {
211
210
  ...forwardedArgs,
212
211
  ];
213
212
  const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
214
- // Inherited stdio, no outer spinner; the delegated installer runs quiet and this parent owns both the plan line and the closing summary.
213
+ // Inherited stdio, no outer spinner; the quiet installer runs, so this parent owns the plan line and closing summary.
215
214
  try {
216
215
  await execInherit(npmCommand, npmCommandArgs);
217
216
  console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION} and add-ons updated.`);