@krtclcdy/tersio 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/cli/banner.js +9 -2
- package/cli/doctor.js +49 -60
- package/cli/install.js +36 -5
- package/cli/usage.js +2 -1
- package/dashboard/app.js +16 -1
- package/extensions/combo-toggle/index.js +20 -5
- package/extensions/shared/plugin-settings.js +4 -0
- package/extensions/shared/pricing.js +112 -70
- package/extensions/shared/usage-ledger.js +2 -2
- package/extensions/tersio-commands/index.js +15 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,10 +52,10 @@ omp plugin features @krtclcdy/tersio --disable rtk
|
|
|
52
52
|
Then restart OMP and enable a preset:
|
|
53
53
|
|
|
54
54
|
```text
|
|
55
|
-
/
|
|
55
|
+
/combo balanced
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
Individual toggles: `/
|
|
58
|
+
Individual toggles: `/caveman full` · `/rtk on` · `/ponytail full`. Everything starts off until you enable it.
|
|
59
59
|
|
|
60
60
|
Session-start defaults (prompted during `install`, or flags):
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ Session-start defaults (prompted during `install`, or flags):
|
|
|
63
63
|
tersio install --combo-default balanced --caveman-default lite --rtk-default on
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Defaults apply to fresh sessions only — anything persisted with `/
|
|
66
|
+
Defaults apply to fresh sessions only — anything persisted with `/combo`, `/caveman`, or `/rtk` wins. Stored as plugin settings (`omp plugin config get @krtclcdy/tersio comboDefault`).
|
|
67
67
|
|
|
68
68
|
One-off use without installing:
|
|
69
69
|
|
|
@@ -116,21 +116,21 @@ Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-defa
|
|
|
116
116
|
|
|
117
117
|
## ⌨️ Commands reference
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
Mode switches live on their own commands; bare `/tersio` prints status.
|
|
120
120
|
|
|
121
121
|
| Command | Purpose |
|
|
122
122
|
|---|---|
|
|
123
|
-
| `/
|
|
124
|
-
| `/
|
|
125
|
-
| `/
|
|
126
|
-
| `/
|
|
123
|
+
| `/combo off\|medium\|balanced\|max\|status` | One preset for all three (start here): medium = lite/lite/on, balanced = full/full/on, max = ultra/ultra/on; per-mode tweaks drop to `custom`. |
|
|
124
|
+
| `/caveman lite\|full\|ultra\|wenyan\|off\|status` | Terse replies (substance stays); `ultra` uses fragments only, `wenyan` a compressed classical-Chinese-inspired register. |
|
|
125
|
+
| `/rtk on\|off\|status` | Compact shell output via the real `rtk` binary; exact bytes (checksums, patches) bypass it by policy. |
|
|
126
|
+
| `/ponytail off\|lite\|full\|ultra\|review\|status` | Minimal code (YAGNI): `lite` nudges, `full` enforces, `ultra` challenges the requirement, `review` audits for over-engineering (upstream command). |
|
|
127
127
|
| `/tersio status` | Active modes + combo level; state persists, propagates to subagents, and lights the Combo footer indicator. |
|
|
128
128
|
| `/tersio check` | Add-on version check |
|
|
129
129
|
| `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | Update add-ons, preview with dry-run |
|
|
130
130
|
| `/tersio gain` | Savings summary; the gain dashboard charts `~/.omp/plugins/tersio-usage.jsonl` (`tersio gain --open` serves on 127.0.0.1, `--export` writes one file). |
|
|
131
131
|
| `/tersio usage` | Ledger report for this machine (rows in `~/.omp/plugins/tersio-usage.jsonl`, local only) |
|
|
132
132
|
| `/tersio help` | This table |
|
|
133
|
-
| `/
|
|
133
|
+
| `/ai-addons` | Add-on updater alias, still works. |
|
|
134
134
|
|
|
135
135
|
## 🗂️ Files and backups
|
|
136
136
|
|
package/cli/banner.js
CHANGED
|
@@ -72,13 +72,20 @@ function bannerLines(tier = bannerTier()) {
|
|
|
72
72
|
return out;
|
|
73
73
|
}
|
|
74
74
|
let shown = false;
|
|
75
|
-
// OMP-style welcome: intro line + note box with art
|
|
75
|
+
// OMP-style welcome: intro line + note box with art left, commands right.
|
|
76
76
|
// No-op unless interactive (and never on --dry-run).
|
|
77
77
|
function printWelcome() {
|
|
78
78
|
if (shown || dryRun || !tty())
|
|
79
79
|
return;
|
|
80
80
|
shown = true;
|
|
81
81
|
clackIntro(`✂ tersio v${PACKAGE_VERSION}`);
|
|
82
|
-
|
|
82
|
+
const tips = [
|
|
83
|
+
'tersio install set up add-ons',
|
|
84
|
+
'tersio update refresh CLI + add-ons',
|
|
85
|
+
'tersio doctor verify the install',
|
|
86
|
+
];
|
|
87
|
+
const art = bannerLines();
|
|
88
|
+
const rows = art.map((line, i) => (i >= 1 && i <= 3 ? `${line} ${tips[i - 1]}` : line));
|
|
89
|
+
clackNote(rows.join('\n'));
|
|
83
90
|
}
|
|
84
91
|
export { bannerLines, bannerTier, printWelcome };
|
package/cli/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// cli/doctor.ts — installation health checks.
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { BUN_BIN_DIR,
|
|
4
|
+
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
5
|
import { runInteractivePhase } from './interactive.js';
|
|
6
6
|
import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
7
7
|
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
@@ -56,50 +56,53 @@ async function runDoctor() {
|
|
|
56
56
|
const err = e;
|
|
57
57
|
return err.stdout?.trim() || err.stderr?.trim() || null;
|
|
58
58
|
}));
|
|
59
|
-
const [ompVersion, agentEntries, extEntries, sharedStateText, configText, updateVersion] = await runInteractivePhase('Checking
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
const [[ompVersion, agentEntries, extEntries, sharedStateText, configText, updateVersion], [cavemanIndexText, rtkIndexText, updaterIndexText, comboIndexText, tersioIndexText, modeReinforcementText, ponytailPkgText, ponytailExtText, pluginsPkgRaw, selfPkgText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
60
|
+
Promise.all([
|
|
61
|
+
probes.ompVersion,
|
|
62
|
+
probes.agentEntries,
|
|
63
|
+
probes.extEntries,
|
|
64
|
+
probes.sharedStateText,
|
|
65
|
+
probes.configText,
|
|
66
|
+
probes.updateVersion,
|
|
67
|
+
]),
|
|
68
|
+
Promise.all([
|
|
69
|
+
probes.cavemanIndexText,
|
|
70
|
+
probes.rtkIndexText,
|
|
71
|
+
probes.updaterIndexText,
|
|
72
|
+
probes.comboIndexText,
|
|
73
|
+
probes.tersioIndexText,
|
|
74
|
+
probes.modeReinforcementText,
|
|
75
|
+
probes.ponytailPkgText,
|
|
76
|
+
probes.ponytailExtText,
|
|
77
|
+
probes.pluginsPkgRaw,
|
|
78
|
+
probes.selfPkgText,
|
|
79
|
+
]),
|
|
80
|
+
Promise.all([
|
|
81
|
+
probes.cavemanRuleText,
|
|
82
|
+
probes.ruleMtime,
|
|
83
|
+
probes.rtkBinText,
|
|
84
|
+
probes.rtkMtime,
|
|
85
|
+
rtkVersionProbe,
|
|
86
|
+
probes.rtkOmpText,
|
|
87
|
+
probes.ponytailMtime,
|
|
88
|
+
]),
|
|
66
89
|
]));
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
probes.rtkIndexText,
|
|
70
|
-
probes.updaterIndexText,
|
|
71
|
-
probes.comboIndexText,
|
|
72
|
-
probes.tersioIndexText,
|
|
73
|
-
probes.modeReinforcementText,
|
|
74
|
-
probes.ponytailPkgText,
|
|
75
|
-
probes.ponytailExtText,
|
|
76
|
-
probes.pluginsPkgRaw,
|
|
77
|
-
probes.selfPkgText,
|
|
78
|
-
]));
|
|
79
|
-
const [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime] = await runInteractivePhase('Checking add-ons', () => Promise.all([
|
|
80
|
-
probes.cavemanRuleText,
|
|
81
|
-
probes.ruleMtime,
|
|
82
|
-
probes.rtkBinText,
|
|
83
|
-
probes.rtkMtime,
|
|
84
|
-
rtkVersionProbe,
|
|
85
|
-
probes.rtkOmpText,
|
|
86
|
-
probes.ponytailMtime,
|
|
87
|
-
]));
|
|
88
|
-
// Categorized output with a tally; section headers group related probes.
|
|
90
|
+
// Categorized output with a tally; success rows stay quiet (no path echoes)
|
|
91
|
+
// while failures print the expected path or fix so they stay actionable.
|
|
89
92
|
const tally = { ok: 0, missing: 0, warn: 0 };
|
|
90
93
|
function check(label, ok, detail = '') {
|
|
91
94
|
if (ok) {
|
|
92
95
|
tally.ok++;
|
|
93
|
-
console.log(` ${label}: ok${detail ? ` ${detail}` : ''}`);
|
|
96
|
+
console.log(` ✅ ${label}: ok${detail ? ` ${detail}` : ''}`);
|
|
94
97
|
}
|
|
95
98
|
else {
|
|
96
99
|
tally.missing++;
|
|
97
|
-
console.log(` ${label}: MISSING${detail ? ` ${detail}` : ''}`);
|
|
100
|
+
console.log(` ❌ ${label}: MISSING${detail ? ` ${detail}` : ''}`);
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
function warnLine(label, detail) {
|
|
101
104
|
tally.warn++;
|
|
102
|
-
console.log(` ${label}: warn ${detail}`);
|
|
105
|
+
console.log(` ⚠️ ${label}: warn ${detail}`);
|
|
103
106
|
}
|
|
104
107
|
section('Environment');
|
|
105
108
|
check('Node', true, process.version);
|
|
@@ -110,33 +113,27 @@ async function runDoctor() {
|
|
|
110
113
|
warnLine('Tersio CLI', `${PACKAGE_VERSION} (version check unreachable)`);
|
|
111
114
|
else
|
|
112
115
|
check('Tersio CLI', true, PACKAGE_VERSION);
|
|
113
|
-
console.log(` Home: ${HOME}`);
|
|
114
116
|
section('Installation');
|
|
115
|
-
check('OMP agent dir', agentEntries !== null, agentDir);
|
|
116
|
-
check('OMP extensions dir', extEntries !== null, extDir);
|
|
117
|
-
check('OMP config.yml', configText !== null, configPath);
|
|
117
|
+
check('OMP agent dir', agentEntries !== null, agentEntries === null ? agentDir : '');
|
|
118
|
+
check('OMP extensions dir', extEntries !== null, extEntries === null ? extDir : '');
|
|
119
|
+
check('OMP config.yml', configText !== null, configText === null ? configPath : '');
|
|
118
120
|
check('Shared session bridge', sharedStateText !== null);
|
|
119
|
-
section('Extensions');
|
|
121
|
+
section('Extensions & plugins');
|
|
120
122
|
check('Caveman extension', cavemanIndexText !== null);
|
|
121
123
|
check('RTK extension', rtkIndexText !== null);
|
|
122
124
|
check('Updater extension', updaterIndexText !== null);
|
|
123
125
|
check('Combo extension', comboIndexText !== null);
|
|
124
126
|
check('Tersio commands extension', tersioIndexText !== null);
|
|
125
127
|
check('Mode reinforcement extension', modeReinforcementText !== null);
|
|
126
|
-
section('Plugins');
|
|
127
|
-
check('Ponytail package', ponytailPkgText !== null);
|
|
128
128
|
check('Ponytail extension', ponytailExtText !== null);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
check('Combo in config.yml', configText.includes('combo-toggle'));
|
|
132
|
-
}
|
|
129
|
+
check('Ponytail in config.yml', (configText ?? '').includes('ponytail') && (configText ?? '').includes('pi-extension'));
|
|
130
|
+
check('Combo in config.yml', (configText ?? '').includes('combo-toggle'));
|
|
133
131
|
check('Self plugin package', selfPkgText !== null, parseJsonObject(selfPkgText)?.version ?? '');
|
|
134
132
|
const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
|
|
135
133
|
check('Self plugin in plugins/package.json', selfDep);
|
|
136
|
-
section('Usage');
|
|
134
|
+
section('Usage & records');
|
|
137
135
|
const usageRows = readUsage();
|
|
138
|
-
check('Usage ledger',
|
|
139
|
-
section('Records');
|
|
136
|
+
check('Usage ledger', true, usageRows.length ? `${usageRows.length} rows` : 'empty — no records yet');
|
|
140
137
|
console.log(` Usage ledger (tersio-owned, tersio reset clears): ${ledgerPath()} · ${usageRows.length} rows`);
|
|
141
138
|
console.log(` Session transcripts (host-owned, never touched): ${sessionsDir()}`);
|
|
142
139
|
const rtkDb = rtkDbPath();
|
|
@@ -146,22 +143,14 @@ async function runDoctor() {
|
|
|
146
143
|
const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
|
|
147
144
|
check('Caveman rule', cavemanRuleText !== null, ruleAge);
|
|
148
145
|
const rtkAge = rtkMtime ? `updated ${relTime(Date.now() - rtkMtime.mtimeMs)}` : '';
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
check('RTK binary', true, bits);
|
|
154
|
-
if (!rtkVersion)
|
|
155
|
-
warnLine('RTK version', 'unavailable — binary may not be executable');
|
|
156
|
-
if (rtkOmpText === null)
|
|
157
|
-
check('RTK OMP wiring (rtk.ts)', false, `run: rtk init -g --agent omp`);
|
|
158
|
-
else
|
|
159
|
-
check('RTK OMP wiring (rtk.ts)', true, path.join(extDir, 'rtk.ts'));
|
|
160
|
-
}
|
|
146
|
+
check('RTK binary', rtkBinText !== null, rtkBinText === null ? rtkBin : [rtkVersion, rtkAge].filter(Boolean).join(', '));
|
|
147
|
+
if (rtkBinText !== null && !rtkVersion)
|
|
148
|
+
warnLine('RTK version', 'unavailable — binary may not be executable');
|
|
149
|
+
check('RTK OMP wiring (rtk.ts)', rtkOmpText !== null, rtkOmpText === null ? 'run: rtk init -g --agent omp' : '');
|
|
161
150
|
const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
|
|
162
151
|
check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
|
|
163
152
|
const total = tally.ok + tally.missing + tally.warn;
|
|
164
|
-
console.log(`\n Summary: ${total} checks — ${tally.ok} ok, ${tally.warn} warn, ${tally.missing} missing`);
|
|
153
|
+
console.log(`\n Summary: ${total} checks — ✅ ${tally.ok} ok, ⚠️ ${tally.warn} warn, ❌ ${tally.missing} missing`);
|
|
165
154
|
}
|
|
166
155
|
function section(name) {
|
|
167
156
|
console.log(`\n${name}`);
|
package/cli/install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { BUN_BIN_DIR, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
6
|
+
import { BUN_BIN_DIR, CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, PONYTAIL_DEFAULTS, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
7
|
import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
|
|
8
8
|
import { printWelcome } from './banner.js';
|
|
9
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
@@ -442,10 +442,41 @@ function defaultProfile() {
|
|
|
442
442
|
ponytailDefault: 'off',
|
|
443
443
|
};
|
|
444
444
|
}
|
|
445
|
-
|
|
446
|
-
|
|
445
|
+
async function storedProfile() {
|
|
446
|
+
const base = defaultProfile();
|
|
447
|
+
const raw = await readTextIfExists(path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json'));
|
|
448
|
+
if (!raw)
|
|
449
|
+
return base;
|
|
450
|
+
let stored;
|
|
451
|
+
try {
|
|
452
|
+
const parsed = JSON.parse(raw);
|
|
453
|
+
if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
|
|
454
|
+
return base;
|
|
455
|
+
const settings = parsed.settings;
|
|
456
|
+
if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
|
|
457
|
+
return base;
|
|
458
|
+
const entry = settings[PACKAGE_NAME];
|
|
459
|
+
if (!entry || typeof entry !== 'object')
|
|
460
|
+
return base;
|
|
461
|
+
stored = entry;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return base;
|
|
465
|
+
}
|
|
466
|
+
if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
|
|
467
|
+
base.comboDefault = stored.comboDefault;
|
|
468
|
+
if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
|
|
469
|
+
base.cavemanDefault = stored.cavemanDefault;
|
|
470
|
+
if (typeof stored.rtkDefault === 'boolean')
|
|
471
|
+
base.rtkDefault = stored.rtkDefault;
|
|
472
|
+
if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
|
|
473
|
+
base.ponytailDefault = stored.ponytailDefault;
|
|
474
|
+
return base;
|
|
475
|
+
}
|
|
447
476
|
async function resolveProfile(forceReinstall = false) {
|
|
448
|
-
|
|
477
|
+
// Seed from the lock file so flag-less update/reinstall runs keep the
|
|
478
|
+
// user's configured defaults instead of resetting them to off.
|
|
479
|
+
const profile = await storedProfile();
|
|
449
480
|
// Single interactive prompt: the Combo preset implies all three modes.
|
|
450
481
|
// Numbered menu — no typing preset names.
|
|
451
482
|
// Only for a real user at a terminal, only when no default flags were
|
|
@@ -456,7 +487,7 @@ async function resolveProfile(forceReinstall = false) {
|
|
|
456
487
|
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
457
488
|
{ value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
|
|
458
489
|
{ value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
459
|
-
],
|
|
490
|
+
], profile.comboDefault);
|
|
460
491
|
if (choice.status === 'selected')
|
|
461
492
|
profile.comboDefault = choice.value;
|
|
462
493
|
else {
|
package/cli/usage.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
-
import { co2GramsFor, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readUsage, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
2
|
+
import { co2GramsFor, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
3
|
import { readRtkGain } from '../extensions/shared/rtk-gain.js';
|
|
4
4
|
import { withInteractiveSpinner } from './interactive.js';
|
|
5
5
|
import { PACKAGE_VERSION } from './common.js';
|
|
6
6
|
export function summarizeUsage(rows) {
|
|
7
|
+
refreshPricesIfStale();
|
|
7
8
|
const byKind = {};
|
|
8
9
|
const detailCounts = {};
|
|
9
10
|
for (const r of rows) {
|
package/dashboard/app.js
CHANGED
|
@@ -879,14 +879,29 @@
|
|
|
879
879
|
els.forEach(function(el) { if (!seen.has(el)) io.observe(el); });
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
+
var lastJson = '';
|
|
882
883
|
function load() {
|
|
883
|
-
|
|
884
|
+
// Served mode only: file:// exports have no data.json endpoint, so
|
|
885
|
+
// polling there would just burn cycles on 404s.
|
|
886
|
+
if (window.location.protocol === 'file:') return;
|
|
887
|
+
// Skip background tabs: no render work while hidden, instant refresh on return.
|
|
888
|
+
if (document.hidden) return;
|
|
889
|
+
fetch('data.json').then(function(r) { return r.json(); }).then(function(d) {
|
|
890
|
+
var json = JSON.stringify(d);
|
|
891
|
+
if (json === lastJson) return;
|
|
892
|
+
lastJson = json;
|
|
893
|
+
render(d);
|
|
894
|
+
}).catch(function() {
|
|
884
895
|
var cards = document.getElementById('modelCards');
|
|
885
896
|
if (cards && !cards.children.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('cloud-off', 'Could not load data', 'Serve with tersio gain instead of opening this file directly.') + '</div>';
|
|
886
897
|
if (window.lucide) lucide.createIcons();
|
|
887
898
|
observe();
|
|
888
899
|
});
|
|
889
900
|
}
|
|
901
|
+
// Live view: re-read data.json every 5s while served and visible; identical
|
|
902
|
+
// payloads skip render. The manual Reload button stays as an instant refresh.
|
|
903
|
+
setInterval(load, 5000);
|
|
904
|
+
document.addEventListener('visibilitychange', function() { if (!document.hidden) load(); });
|
|
890
905
|
function toast(title, desc, icon) {
|
|
891
906
|
var wrap = document.querySelector('.toaster');
|
|
892
907
|
if (!wrap) {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
-
import { activeModesSummary, asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, systemPromptIncludes, } from '../shared/session-state.js';
|
|
7
|
-
import { readComboDefault } from '../shared/plugin-settings.js';
|
|
6
|
+
import { activeModesSummary, asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
|
|
7
|
+
import { readComboDefault, readPonytailDefault } from '../shared/plugin-settings.js';
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
10
10
|
lite: 'Prefer the simplest correct solution.',
|
|
@@ -120,9 +120,13 @@ export default function comboToggleExtension(pi) {
|
|
|
120
120
|
track(ctx);
|
|
121
121
|
if (!ctx?.hasUI)
|
|
122
122
|
syncStatus(ctx);
|
|
123
|
-
// Installer/user-configured default applies only
|
|
124
|
-
//
|
|
125
|
-
|
|
123
|
+
// Installer/user-configured default applies only when no persisted *mode*
|
|
124
|
+
// state exists — unrelated session entries must not block it, or the
|
|
125
|
+
// combo bar never paints on sessions that already carry other entries.
|
|
126
|
+
const entries = sessionEntries(ctx);
|
|
127
|
+
const hasModeState = entries.some((e) => e?.type === 'custom' && (e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
|
|
128
|
+
e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
|
|
129
|
+
if (getSharedComboState().level === 'off' && !hasModeState) {
|
|
126
130
|
const fallback = readComboDefault();
|
|
127
131
|
if (fallback !== 'off') {
|
|
128
132
|
persistPreset(fallback);
|
|
@@ -130,6 +134,17 @@ export default function comboToggleExtension(pi) {
|
|
|
130
134
|
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
131
135
|
}
|
|
132
136
|
}
|
|
137
|
+
// Standalone ponytail default: no sibling extension restores it (upstream
|
|
138
|
+
// owns the command), so apply it here when nothing persisted a
|
|
139
|
+
// ponytail-mode entry. Skipped when it matches the active preset — the
|
|
140
|
+
// preset entry already covers that, no redundant write.
|
|
141
|
+
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
142
|
+
const ponytailFallback = readPonytailDefault();
|
|
143
|
+
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
144
|
+
pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
|
|
145
|
+
useState(setSharedComboMode('ponytail', ponytailFallback), ctx);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
133
148
|
});
|
|
134
149
|
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
135
150
|
pi.on(event, async (_event, ctx) => {
|
|
@@ -31,6 +31,7 @@ export function readPluginSettings() {
|
|
|
31
31
|
}
|
|
32
32
|
const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
|
|
33
33
|
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
|
|
34
|
+
const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra', 'review']);
|
|
34
35
|
function readStringDefault(key, valid) {
|
|
35
36
|
const raw = readPluginSettings()[key];
|
|
36
37
|
return typeof raw === 'string' && valid.has(raw) ? raw : 'off';
|
|
@@ -45,3 +46,6 @@ export function readRtkDefault() {
|
|
|
45
46
|
const raw = readPluginSettings().rtkDefault;
|
|
46
47
|
return typeof raw === 'boolean' ? raw : false;
|
|
47
48
|
}
|
|
49
|
+
export function readPonytailDefault() {
|
|
50
|
+
return readStringDefault('ponytailDefault', PONYTAIL_MODES);
|
|
51
|
+
}
|
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
// extensions/shared/pricing.ts — model pricing
|
|
2
|
-
// Lookup order: exact id in the live cache →
|
|
3
|
-
//
|
|
4
|
-
// sync and offline-safe.
|
|
1
|
+
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM.
|
|
2
|
+
// Lookup order: exact id in the live cache → provider-prefix strip → default.
|
|
3
|
+
// The cache is the full LiteLLM feed (3k+ ids), refreshed lazily in background
|
|
4
|
+
// and on `tersio update`; every reader is sync and offline-safe. No static
|
|
5
|
+
// per-model table — list prices rot, the feed does not. Unknown models report
|
|
6
|
+
// the default with known:false so the dashboard labels them honestly.
|
|
5
7
|
import fs from 'node:fs';
|
|
6
8
|
import os from 'node:os';
|
|
7
9
|
import path from 'node:path';
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
const PRICE_TABLE = [
|
|
11
|
-
{ match: ':free', price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
|
|
12
|
-
{ match: 'opus', price: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 } },
|
|
13
|
-
{ match: 'sonnet', price: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } },
|
|
14
|
-
{ match: 'haiku', price: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 } },
|
|
15
|
-
{ match: 'gpt', price: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 2.5 } },
|
|
16
|
-
{ match: 'gemini', price: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 1.25 } },
|
|
17
|
-
{ match: 'glm', price: { input: 0.5, output: 1, cacheRead: 0.05, cacheWrite: 0.5 } },
|
|
18
|
-
];
|
|
19
|
-
export const DEFAULT_PRICE = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
10
|
+
// Sonnet-class default for unpriced models; always paired with known:false.
|
|
11
|
+
export const DEFAULT_PRICE = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
|
|
20
12
|
const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
13
|
+
// Live cache refreshes lazily: readers use the file even when stale, and only
|
|
14
|
+
// one refresh runs per process. `tersio update` still forces a fresh fetch.
|
|
21
15
|
const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
22
|
-
|
|
16
|
+
let refreshInflight = null;
|
|
23
17
|
export function pricesCachePath() {
|
|
24
18
|
const override = process.env.TERSIO_PRICES_FILE;
|
|
25
19
|
if (override)
|
|
@@ -29,6 +23,23 @@ export function pricesCachePath() {
|
|
|
29
23
|
export function pricesUrl() {
|
|
30
24
|
return process.env.TERSIO_PRICES_URL || LITELLM_URL;
|
|
31
25
|
}
|
|
26
|
+
function toPrice(t) {
|
|
27
|
+
return { input: t[0], output: t[1], cacheRead: t[2], cacheWrite: t[3] };
|
|
28
|
+
}
|
|
29
|
+
// Tolerate both cache shapes: compact tuples (new) and ModelPrice objects
|
|
30
|
+
// (written by older refreshes) — a version bump never orphans the cache.
|
|
31
|
+
function asPrice(v) {
|
|
32
|
+
if (Array.isArray(v) && v.length === 4 && v.every((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)) {
|
|
33
|
+
return toPrice(v);
|
|
34
|
+
}
|
|
35
|
+
if (v && typeof v === 'object') {
|
|
36
|
+
const o = v;
|
|
37
|
+
if (['input', 'output', 'cacheRead', 'cacheWrite'].every((k) => typeof o[k] === 'number' && Number.isFinite(o[k]) && o[k] >= 0)) {
|
|
38
|
+
return o;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
32
43
|
export function loadLivePrices() {
|
|
33
44
|
let raw;
|
|
34
45
|
try {
|
|
@@ -41,9 +52,17 @@ export function loadLivePrices() {
|
|
|
41
52
|
const parsed = JSON.parse(raw);
|
|
42
53
|
if (typeof parsed.fetchedAt !== 'number' || !parsed.exact || typeof parsed.exact !== 'object')
|
|
43
54
|
return null;
|
|
44
|
-
|
|
55
|
+
const exact = {};
|
|
56
|
+
for (const [k, v] of Object.entries(parsed.exact)) {
|
|
57
|
+
const p = asPrice(v);
|
|
58
|
+
if (p)
|
|
59
|
+
exact[k] = [p.input, p.output, p.cacheRead, p.cacheWrite];
|
|
60
|
+
}
|
|
61
|
+
if (!Object.keys(exact).length)
|
|
45
62
|
return null;
|
|
46
|
-
|
|
63
|
+
// Stale entries stay usable; refreshPricesIfStale refreshes in background.
|
|
64
|
+
const deprecated = Array.isArray(parsed.deprecated) ? parsed.deprecated.filter((d) => typeof d === 'string') : [];
|
|
65
|
+
return { fetchedAt: parsed.fetchedAt, exact, deprecated };
|
|
47
66
|
}
|
|
48
67
|
catch {
|
|
49
68
|
return null;
|
|
@@ -55,67 +74,90 @@ function num(v) {
|
|
|
55
74
|
export function priceFor(model, live) {
|
|
56
75
|
const table = live === undefined ? loadLivePrices() : live;
|
|
57
76
|
if (table) {
|
|
58
|
-
const hit = table.exact[model] ?? table.exact[model.toLowerCase()];
|
|
59
|
-
if (hit
|
|
77
|
+
const hit = asPrice(table.exact[model] ?? table.exact[model.toLowerCase()]);
|
|
78
|
+
if (hit)
|
|
60
79
|
return { price: hit, known: true, live: true };
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
// Provider-prefixed ids ("azure/gpt-4o", "dashscope/qwen-max"): match the
|
|
81
|
+
// first cached id that ends with the bare model name.
|
|
82
|
+
const name = model.toLowerCase();
|
|
83
|
+
const key = Object.keys(table.exact).find((k) => k.toLowerCase() === name || k.toLowerCase().endsWith(`/${name}`));
|
|
84
|
+
const prefixed = key ? asPrice(table.exact[key]) : null;
|
|
85
|
+
if (prefixed)
|
|
86
|
+
return { price: prefixed, known: true, live: true };
|
|
67
87
|
}
|
|
68
88
|
return { price: DEFAULT_PRICE, known: false, live: false };
|
|
69
89
|
}
|
|
70
|
-
// Fetch LiteLLM pricing, keep
|
|
71
|
-
// any failure (offline, timeout, shape change) — callers
|
|
90
|
+
// Fetch LiteLLM pricing, keep every id with valid input/output rates, write
|
|
91
|
+
// the cache. False on any failure (offline, timeout, shape change) — callers
|
|
92
|
+
// fall back silently.
|
|
72
93
|
export async function refreshPrices() {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
if (refreshInflight)
|
|
95
|
+
return refreshInflight;
|
|
96
|
+
refreshInflight = (async () => {
|
|
97
|
+
let res;
|
|
77
98
|
try {
|
|
78
|
-
|
|
99
|
+
const ctl = new AbortController();
|
|
100
|
+
const timer = setTimeout(() => ctl.abort(), 15000);
|
|
101
|
+
try {
|
|
102
|
+
res = await fetch(pricesUrl(), { signal: ctl.signal });
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
if (!res.ok)
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
79
112
|
}
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
let data;
|
|
114
|
+
try {
|
|
115
|
+
data = (await res.json());
|
|
82
116
|
}
|
|
83
|
-
|
|
117
|
+
catch {
|
|
84
118
|
return false;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
exact
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
}
|
|
120
|
+
const exact = {};
|
|
121
|
+
const deprecated = [];
|
|
122
|
+
for (const [id, row] of Object.entries(data)) {
|
|
123
|
+
if (!row || typeof row !== 'object')
|
|
124
|
+
continue;
|
|
125
|
+
const input = num(row.input_cost_per_token);
|
|
126
|
+
const output = num(row.output_cost_per_token);
|
|
127
|
+
if (input === null || output === null)
|
|
128
|
+
continue;
|
|
129
|
+
exact[id] = [
|
|
130
|
+
input * 1e6,
|
|
131
|
+
output * 1e6,
|
|
132
|
+
(num(row.cache_read_input_token_cost) ?? input) * 1e6,
|
|
133
|
+
(num(row.cache_creation_input_token_cost) ?? input) * 1e6,
|
|
134
|
+
];
|
|
135
|
+
if (typeof row.deprecation_date === 'string' && row.deprecation_date)
|
|
136
|
+
deprecated.push(id);
|
|
137
|
+
}
|
|
138
|
+
if (!Object.keys(exact).length)
|
|
139
|
+
return false;
|
|
140
|
+
try {
|
|
141
|
+
fs.mkdirSync(path.dirname(pricesCachePath()), { recursive: true });
|
|
142
|
+
fs.writeFileSync(pricesCachePath(), JSON.stringify({ fetchedAt: Date.now(), exact, deprecated }), 'utf8');
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
113
149
|
try {
|
|
114
|
-
|
|
115
|
-
fs.writeFileSync(pricesCachePath(), JSON.stringify({ fetchedAt: Date.now(), exact }), 'utf8');
|
|
116
|
-
return true;
|
|
150
|
+
return await refreshInflight;
|
|
117
151
|
}
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
finally {
|
|
153
|
+
refreshInflight = null;
|
|
120
154
|
}
|
|
121
155
|
}
|
|
156
|
+
// Fire-and-forget refresh when the cache is stale or missing. Readers keep
|
|
157
|
+
// using the stale file (or the built-in table) — pricing never blocks.
|
|
158
|
+
export function refreshPricesIfStale() {
|
|
159
|
+
const live = loadLivePrices();
|
|
160
|
+
if (live && Date.now() - live.fetchedAt <= CACHE_TTL_MS)
|
|
161
|
+
return;
|
|
162
|
+
void refreshPrices().catch(() => false);
|
|
163
|
+
}
|
|
@@ -215,9 +215,9 @@ function leadBinary(command) {
|
|
|
215
215
|
return 'bash';
|
|
216
216
|
}
|
|
217
217
|
// --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
|
|
218
|
-
import { DEFAULT_PRICE, priceFor } from './pricing.js';
|
|
218
|
+
import { DEFAULT_PRICE, priceFor, refreshPricesIfStale } from './pricing.js';
|
|
219
219
|
import { co2GramsFor, energyWhFor } from './carbon.js';
|
|
220
|
-
export { DEFAULT_PRICE, priceFor };
|
|
220
|
+
export { DEFAULT_PRICE, priceFor, refreshPricesIfStale };
|
|
221
221
|
export { co2GramsFor, energyWhFor };
|
|
222
222
|
export function usdCost(t, model) {
|
|
223
223
|
const { price, known } = model ? priceFor(model) : { price: DEFAULT_PRICE, known: false };
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
// /tersio
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// sibling
|
|
5
|
-
//
|
|
6
|
-
import {
|
|
1
|
+
// /tersio root command — status, check, update, gain, usage, help.
|
|
2
|
+
// Mode switches live on their own commands (/caveman, /rtk, /combo, and the
|
|
3
|
+
// upstream /ponytail): the router keeps no redundant copies. Shared-state
|
|
4
|
+
// publishes sync sibling mirrors live, so switches notify and take effect
|
|
5
|
+
// next turn — no reload.
|
|
6
|
+
import { getSharedComboState, reconcileSharedComboEntries, sessionEntries, } from '../shared/session-state.js';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { appendUsage, readUsage } from '../shared/usage-ledger.js';
|
|
10
10
|
import { checkAddonsSummary, runAddonUpdate } from '../ai-addons-updater/index.js';
|
|
11
11
|
const HELP = [
|
|
12
|
-
'/tersio caveman lite|full|ultra|wenyan|off — terse-reply mode',
|
|
13
|
-
'/tersio combo off|medium|balanced|max — preset for all three modes',
|
|
14
|
-
'/tersio rtk on|off — compact shell output',
|
|
15
|
-
'/tersio ponytail off|lite|full|ultra|review — minimal code',
|
|
16
12
|
'/tersio status — active modes + combo level',
|
|
17
13
|
'/tersio check — add-on version check',
|
|
18
14
|
'/tersio update <ponytail|rtk|caveman|all> [--dry-run]',
|
|
19
15
|
'/tersio gain — savings summary + dashboard hint',
|
|
20
16
|
'/tersio usage — ledger report for this machine',
|
|
21
17
|
'/tersio help — this table',
|
|
22
|
-
'
|
|
18
|
+
'mode switches: /caveman /rtk /combo (tersio) + /ponytail (upstream)',
|
|
23
19
|
].join('\n');
|
|
24
20
|
function statusLine() {
|
|
25
21
|
const s = getSharedComboState();
|
|
@@ -56,14 +52,11 @@ async function openGainDashboard(pi, ctx) {
|
|
|
56
52
|
}
|
|
57
53
|
export default function tersioCommandsExtension(pi) {
|
|
58
54
|
pi.setLabel?.('Tersio unified root command');
|
|
59
|
-
// Mode switches never reload the session: entries persist, the shared
|
|
60
|
-
// bridge publishes to every sibling mirror, and the change injects on the
|
|
61
|
-
// next turn like a normal input message. Only notify below.
|
|
62
55
|
pi.registerCommand?.('tersio', {
|
|
63
|
-
description: 'Tersio root:
|
|
56
|
+
description: 'Tersio root: status|check|update|gain|usage|help (modes: /caveman /rtk /combo /ponytail)',
|
|
64
57
|
handler: async (args, ctx) => {
|
|
65
58
|
const parts = String(args || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
66
|
-
const [sub
|
|
59
|
+
const [sub] = [parts[0]];
|
|
67
60
|
appendUsage('command', `/tersio ${parts.join(' ')}`.trim());
|
|
68
61
|
if (!sub || sub === 'status') {
|
|
69
62
|
if (ctx?.hasUI)
|
|
@@ -75,69 +68,6 @@ export default function tersioCommandsExtension(pi) {
|
|
|
75
68
|
ctx?.ui?.notify?.(HELP, 'info');
|
|
76
69
|
return;
|
|
77
70
|
}
|
|
78
|
-
if (sub === 'caveman') {
|
|
79
|
-
const mode = normalizeMode('caveman', rest);
|
|
80
|
-
if (!mode) {
|
|
81
|
-
ctx?.ui?.notify?.('Usage: /tersio caveman [lite|full|ultra|wenyan|off|status]', 'warning');
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
pi.appendEntry?.('caveman-mode', { mode });
|
|
85
|
-
setSharedComboMode('caveman', mode);
|
|
86
|
-
appendUsage('toggle', `caveman=${mode}`);
|
|
87
|
-
ctx?.ui?.notify?.(`Caveman ${mode} on — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (sub === 'rtk') {
|
|
91
|
-
if (!rest || rest === 'status') {
|
|
92
|
-
const on = getSharedComboState().rtk === 'on';
|
|
93
|
-
ctx?.ui?.notify?.(`RTK: ${on ? 'on' : 'off'}`, 'info');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (!['on', 'off'].includes(rest)) {
|
|
97
|
-
ctx?.ui?.notify?.('Usage: /tersio rtk [on|off|status]', 'warning');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const enabled = rest === 'on';
|
|
101
|
-
pi.appendEntry?.('rtk-mode', { enabled });
|
|
102
|
-
setSharedComboMode('rtk', enabled);
|
|
103
|
-
appendUsage('toggle', `rtk=${rest}`);
|
|
104
|
-
ctx?.ui?.notify?.(`RTK ${rest} — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (sub === 'ponytail') {
|
|
108
|
-
const mode = normalizeMode('ponytail', rest);
|
|
109
|
-
if (!mode) {
|
|
110
|
-
ctx?.ui?.notify?.('Usage: /tersio ponytail [off|lite|full|ultra|review|status]', 'warning');
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
pi.appendEntry?.('ponytail-mode', { mode });
|
|
114
|
-
setSharedComboMode('ponytail', mode);
|
|
115
|
-
appendUsage('toggle', `ponytail=${mode}`);
|
|
116
|
-
ctx?.ui?.notify?.(`Ponytail ${mode} — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (sub === 'combo') {
|
|
120
|
-
if (!rest || rest === 'status') {
|
|
121
|
-
if (ctx?.hasUI)
|
|
122
|
-
reconcileSharedComboEntries(sessionEntries(ctx));
|
|
123
|
-
ctx?.ui?.notify?.(statusLine(), 'info');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const level = normalizeComboLevel(rest);
|
|
127
|
-
if (!level) {
|
|
128
|
-
ctx?.ui?.notify?.('Unknown combo level. Use: off | medium | balanced | max', 'warning');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const modes = COMBO_LEVELS[level];
|
|
132
|
-
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
133
|
-
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
134
|
-
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
135
|
-
pi.appendEntry?.('combo-level', { level });
|
|
136
|
-
setSharedComboLevel(level);
|
|
137
|
-
appendUsage('toggle', `combo=${level}`);
|
|
138
|
-
ctx?.ui?.notify?.(`Combo ${level} on — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
71
|
if (sub === 'check') {
|
|
142
72
|
await checkAddonsSummary(ctx);
|
|
143
73
|
return;
|
|
@@ -160,6 +90,12 @@ export default function tersioCommandsExtension(pi) {
|
|
|
160
90
|
ctx?.ui?.notify?.(usageSummary(), 'info');
|
|
161
91
|
return;
|
|
162
92
|
}
|
|
93
|
+
// Removed mode switches redirect to their own commands so only one
|
|
94
|
+
// spelling exists to learn: /caveman, /rtk, /combo, /ponytail.
|
|
95
|
+
if (sub === 'caveman' || sub === 'rtk' || sub === 'combo' || sub === 'ponytail') {
|
|
96
|
+
ctx?.ui?.notify?.(`Use /${sub} instead — /tersio no longer duplicates mode switches.\n${HELP}`, 'warning');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
163
99
|
ctx?.ui?.notify?.(`Unknown subcommand: ${sub}.\n${HELP}`, 'warning');
|
|
164
100
|
},
|
|
165
101
|
});
|
package/package.json
CHANGED