@krtclcdy/tersio 2.4.1 → 2.6.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 CHANGED
@@ -33,10 +33,10 @@ omp plugin features @krtclcdy/tersio --disable rtk
33
33
  Then restart OMP and enable a preset:
34
34
 
35
35
  ```text
36
- /combo medium
36
+ /tersio combo medium
37
37
  ```
38
38
 
39
- Individual toggles: `/caveman full` · `/rtk on` · `/ponytail full`. Everything starts off until you enable it.
39
+ Individual toggles: `/tersio caveman full` · `/tersio rtk on` · `/tersio ponytail full`. Everything starts off until you enable it.
40
40
 
41
41
  Session-start defaults (prompted during `install`, or flags):
42
42
 
@@ -44,7 +44,7 @@ Session-start defaults (prompted during `install`, or flags):
44
44
  tersio install --combo-default balanced --caveman-default lite --rtk-default on
45
45
  ```
46
46
 
47
- 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`).
47
+ Defaults apply to fresh sessions only — anything persisted with `/tersio combo`, `/tersio caveman`, or `/tersio rtk` wins. Stored as plugin settings (`omp plugin config get @krtclcdy/tersio comboDefault`).
48
48
 
49
49
  ## What it installs
50
50
 
@@ -54,7 +54,7 @@ Defaults apply to fresh sessions only — anything persisted with `/combo`, `/ca
54
54
  | **RTK** | Routes noisy shell commands through the RTK binary for compact output |
55
55
  | **Ponytail** | Minimal, YAGNI-oriented code decisions |
56
56
  | **Combo** | Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max` |
57
- | **Updater** | `/ai-addons` checks and updates Ponytail, RTK, and Caveman in-session, with dry-run and backups |
57
+ | **Updater** | `/tersio check` and `/tersio update` cover Ponytail, RTK, and Caveman in-session, with dry-run and backups |
58
58
 
59
59
  User-level installs also register the package in `~/.omp/plugins` (visible in OMP **Settings → Plugins**), directly through the plugin manifest. Active modes reassert after Ponytail's prompt block on every top-level turn, including after history compaction.
60
60
 
@@ -81,6 +81,8 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead
81
81
  | `tersio update` | Refresh the CLI, extensions, and add-ons (RTK binary, Caveman rule, Ponytail) |
82
82
  | `tersio reinstall` | Fresh install, preserving the Ponytail package |
83
83
  | `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
84
+ | `tersio usage` | Ledger-backed usage + savings report |
85
+ | `tersio dashboard` | Serve the gain dashboard on localhost (`--open`, `--export <file>`, `--port <n>`) |
84
86
  | `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
85
87
  | `tersio version` | Print version |
86
88
 
@@ -88,63 +90,44 @@ Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--scope`, `--combo-default`/`--c
88
90
 
89
91
  ## Commands reference
90
92
 
91
- ### Caveman — terse replies
93
+ Everything runs under one root. Bare `/tersio` prints status.
92
94
 
93
- ```text
94
- /caveman lite concise, drops pleasantries
95
- /caveman full terse caveman style
96
- /caveman ultra maximum terse, fragments only
97
- /caveman wenyan classical-Chinese-style where clear
98
- /caveman off normal mode
99
- /caveman status show current mode
100
- ```
95
+ | Command | Purpose |
96
+ |---|---|
97
+ | `/tersio combo off\|medium\|balanced\|max\|status` | preset for all three modes (start here) |
98
+ | `/tersio caveman lite\|full\|ultra\|wenyan\|off\|status` | terse-reply mode |
99
+ | `/tersio rtk on\|off\|status` | compact shell-output toggle |
100
+ | `/tersio ponytail off\|lite\|full\|ultra\|review\|status` | minimal-code level |
101
+ | `/tersio status` | active modes + combo level |
102
+ | `/tersio check` | add-on version check (same as old `/ai-addons check`) |
103
+ | `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | update add-ons, preview with dry-run |
104
+ | `/tersio gain` | savings summary + dashboard hint |
105
+ | `/tersio usage` | ledger report for this machine |
106
+ | `/tersio help` | this table |
101
107
 
102
- `caveman off`, `stop caveman`, and `normal mode` also work.
108
+ ### Combo — one preset for all three
103
109
 
104
- ### RTK — compact shell output
110
+ Start here: one setting drives all three modes (`medium` = lite/lite/on, `balanced` = full/full/on, `max` = ultra/ultra/on, `off` = normal). Tweaking a mode individually drops to a `custom` mix.
105
111
 
106
- ```text
107
- /rtk on enable compact RTK output
108
- /rtk off disable
109
- /rtk status show current state
110
- ```
112
+ ### Caveman — terse replies
111
113
 
112
- Covers noisy commands (`git status`, `git diff`, `read`, `grep`, test, `tsc`, lint).
114
+ Pleasantries and restatements go; substance stays. Ladder: `lite`, `full`, `ultra` (fragments only), `wenyan` (compressed classical-Chinese-inspired register).
113
115
 
114
- ### Ponytail — minimal code
116
+ ### RTK — compact shell output
115
117
 
116
- ```text
117
- /ponytail lite light guidance
118
- /ponytail full full YAGNI enforcement
119
- /ponytail ultra aggressive simplification
120
- /ponytail off disable
121
- /ponytail status show current state
122
- ```
118
+ A real binary, not a prompt trick: `rtk git status|diff|grep|test|tsc|lint` strips noise before output reaches the model. Exact bytes (checksums, patches) bypass it by policy.
123
119
 
124
- ### Updater — check and update add-ons
120
+ ### Ponytail — minimal code (YAGNI)
125
121
 
126
- ```text
127
- /ai-addons check check all add-on versions
128
- /ai-addons status same as check
129
- /ai-addons update ponytail update Ponytail
130
- /ai-addons update rtk update the RTK binary
131
- /ai-addons update caveman update the Caveman rule
132
- /ai-addons update all update all three
133
- /ai-addons update all --dry-run preview without changes
134
- ```
122
+ You Aren't Gonna Need It: smallest working change, no speculative abstractions, no new deps for stdlib jobs. `lite` nudges, `full` enforces, `ultra` challenges the requirement, `review` audits for over-engineering.
135
123
 
136
- ### Combo — toggle all three
124
+ `/tersio` persists state and reloads OMP without emitting separate command messages. Presets propagate to task subagents and light the Combo footer indicator; mixed individual settings report via `/tersio status`.
137
125
 
138
- ```text
139
- /combo off all three off (default)
140
- /combo medium caveman=lite, rtk=on, ponytail=lite
141
- /combo balanced caveman=full, rtk=on, ponytail=full
142
- /combo max caveman=ultra, rtk=on, ponytail=ultra
143
- /combo status show the level and underlying modes
144
- /combo help show available levels
145
- ```
126
+ ### Legacy aliases (still work)
127
+
128
+ The pre-`/tersio` commands remain registered: `/caveman`, `/rtk`, `/combo`, `/ai-addons`. Prefer `/tersio` in new muscle memory; help output lists the aliases as deprecated-but-working.
146
129
 
147
- `/combo` persists state and reloads OMP without emitting separate command messages. Presets propagate to task subagents and light the Combo footer indicator; individual commands leave Combo inactive (`/combo status` still reports the mixed state).
130
+ Usage rows land in `~/.omp/plugins/tersio-usage.jsonl` (local only); `tersio dashboard` serves them as a plain-HTML page on 127.0.0.1.
148
131
 
149
132
  ## Files and backups
150
133
 
@@ -176,7 +159,7 @@ npm exec --yes --prefer-online --package=@krtclcdy/tersio@latest -- tersio insta
176
159
 
177
160
  **RTK missing or not executable:** run `tersio reinstall`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
178
161
 
179
- **Checksum warning or failure:** the installer aborts on RTK checksum mismatch but warns and continues when checksum metadata is unavailable; `/ai-addons update rtk` aborts when metadata is missing.
162
+ **Checksum warning or failure:** the installer aborts on RTK checksum mismatch but warns and continues when checksum metadata is unavailable; `/tersio update rtk` aborts when metadata is missing.
180
163
 
181
164
  ## License
182
165
 
package/cli/brand.webp ADDED
Binary file
package/cli/common.js ADDED
@@ -0,0 +1,257 @@
1
+ // cli/common.ts — shared foundation: CLI flags, paths, exec, file/config helpers.
2
+ import { execFile } from 'node:child_process';
3
+ import { promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { createRequire } from 'node:module';
7
+ import { readTextIfExists } from '../extensions/lib/utils.js';
8
+ const IS_WINDOWS = process.platform === 'win32';
9
+ const RTK_BINARY_NAME = IS_WINDOWS ? 'rtk.exe' : 'rtk';
10
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
11
+ const OMP_AGENT_DIR = path.join(HOME, '.omp', 'agent');
12
+ const OMP_PLUGINS_DIR = path.join(HOME, '.omp', 'plugins');
13
+ const BUN_BIN_DIR = path.join(HOME, '.bun', 'bin');
14
+ const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
15
+ const PACKAGE_NAME = '@krtclcdy/tersio';
16
+ const PACKAGE_BIN = 'tersio';
17
+ const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('../package.json');
18
+ // Session-start mode defaults for fresh installs.
19
+ const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
20
+ const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'review']);
21
+ const RTK_DEFAULTS = new Set(['on', 'off']);
22
+ // ponytail: Combo preset implies all three modes. Mirrors COMBO_LEVELS in
23
+ // extensions/shared/session-state.ts (rtk as boolean here). Single source.
24
+ const COMBO_PRESET_MODES = {
25
+ off: { caveman: 'off', rtk: false, ponytail: 'off' },
26
+ medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
27
+ balanced: { caveman: 'full', rtk: true, ponytail: 'full' },
28
+ max: { caveman: 'ultra', rtk: true, ponytail: 'ultra' },
29
+ };
30
+ const COMBO_DEFAULTS = new Set(Object.keys(COMBO_PRESET_MODES));
31
+ function parseEnum(value, valid, flag) {
32
+ if (value === undefined)
33
+ return undefined;
34
+ const v = value.trim().toLowerCase();
35
+ if (!valid.has(v)) {
36
+ console.error(`[fail] Invalid ${flag}: ${value}. Valid: ${[...valid].join(', ')}`);
37
+ process.exit(1);
38
+ }
39
+ return v;
40
+ }
41
+ function flagValue(name) {
42
+ const i = args.indexOf(name);
43
+ if (i !== -1)
44
+ return args[i + 1];
45
+ const prefix = `${name}=`;
46
+ return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
47
+ }
48
+ // --- CLI flags ---
49
+ const COMMANDS = new Set(['install', 'update', 'reinstall', 'doctor', 'uninstall', 'usage', 'dashboard', 'version', 'help']);
50
+ const args = process.argv.slice(2);
51
+ const commandArg = args.find((arg, index) => !arg.startsWith('-') && args[index - 1] !== '--scope');
52
+ const command = commandArg?.toLowerCase() || null;
53
+ const unknownCommand = command !== null && !COMMANDS.has(command);
54
+ const install = command === 'install';
55
+ const update = command === 'update';
56
+ const reinstall = command === 'reinstall';
57
+ const showVersion = command === 'version' || args.includes('--version') || args.includes('-v');
58
+ const showHelp = command === 'help' || args.includes('--help') || args.includes('-h');
59
+ const applyUpdate = args.includes('--apply-update');
60
+ const dryRun = args.includes('--dry-run');
61
+ const yes = args.includes('--yes') || args.includes('-y') || install || update || reinstall || applyUpdate;
62
+ const verbose = args.includes('--verbose');
63
+ const doctor = command === 'doctor' || args.includes('--doctor');
64
+ const usage = command === 'usage' || args.includes('--usage');
65
+ const dashboard = command === 'dashboard' || args.includes('--dashboard');
66
+ const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
67
+ const dashboardOpen = args.includes('--open');
68
+ const dashboardExport = flagValue('--export') ?? null;
69
+ const uninstall = command === 'uninstall' || args.includes('--uninstall');
70
+ const removePonytail = args.includes('--remove-ponytail');
71
+ const keepPonytail = args.includes('--keep-ponytail');
72
+ const removeRtk = args.includes('--remove-rtk');
73
+ const scopeFlag = flagValue('--scope')?.toLowerCase() ?? null;
74
+ // --- Profile selection (session-start mode defaults) ---
75
+ const comboDefaultFlag = parseEnum(flagValue('--combo-default'), COMBO_DEFAULTS, '--combo-default');
76
+ const cavemanDefaultFlag = parseEnum(flagValue('--caveman-default'), CAVEMAN_DEFAULTS, '--caveman-default');
77
+ const ponytailDefaultFlag = parseEnum(flagValue('--ponytail-default'), PONYTAIL_DEFAULTS, '--ponytail-default');
78
+ const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rtk-default');
79
+ const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
80
+ .some((flag) => flag !== undefined);
81
+ function debug(...a) {
82
+ if (verbose)
83
+ console.log(' [debug]', ...a);
84
+ }
85
+ const execFileP = promisify(execFile);
86
+ async function execP(cmd, args, opts = {}) {
87
+ return execFileP(cmd, args, {
88
+ timeout: opts.timeout || 120000,
89
+ encoding: 'utf8',
90
+ ...opts,
91
+ });
92
+ }
93
+ async function writeIfChanged(dest, content, options = {}) {
94
+ const existing = await readTextIfExists(dest);
95
+ if (existing === content) {
96
+ debug(`${dest} already up to date`);
97
+ return false;
98
+ }
99
+ if (options.dryRun) {
100
+ console.log(` [dry-run] would write ${dest}`);
101
+ return true;
102
+ }
103
+ await fs.mkdir(path.dirname(dest), { recursive: true });
104
+ if (existing !== null) {
105
+ await fs.copyFile(dest, `${dest}.bak`);
106
+ debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
107
+ }
108
+ await fs.writeFile(dest, content, 'utf8');
109
+ console.log(` [write] ${dest}`);
110
+ return true;
111
+ }
112
+ // omp ships 'extensions: null'; appending list items under a null scalar breaks
113
+ // YAML parsing. Normalize null/~/[]/empty to a mapping key first.
114
+ const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
115
+ const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
116
+ function normalizeExtensionsKey(lines) {
117
+ const idx = lines.findIndex((l) => EXTENSIONS_NULL_RE.test(l));
118
+ if (idx === -1)
119
+ return false;
120
+ lines[idx] = 'extensions:';
121
+ return true;
122
+ }
123
+ async function writeConfigLines(configPath, lines, logMsg) {
124
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
125
+ await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
126
+ await fs.writeFile(configPath, lines.join('\n'), 'utf8');
127
+ console.log(logMsg);
128
+ }
129
+ async function ensureExtensionInConfig(configPath, extensionPath, label, options = {}) {
130
+ const normalizedPath = extensionPath.replace(/\\/g, '/');
131
+ const line = ` - ${normalizedPath}`;
132
+ const raw = await readTextIfExists(configPath);
133
+ let lines = (raw || '').split('\n');
134
+ if (lines.some((l) => l.includes(normalizedPath))) {
135
+ debug(`${label} already in config.yml`);
136
+ return false;
137
+ }
138
+ const extLineIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
139
+ if (options.dryRun) {
140
+ console.log(` [dry-run] would add ${label} to config.yml: ${normalizedPath}`);
141
+ return true;
142
+ }
143
+ if (normalizeExtensionsKey(lines)) {
144
+ lines.splice(extLineIdx + 1, 0, line, '');
145
+ }
146
+ else if (extLineIdx === -1) {
147
+ lines.push('extensions:');
148
+ lines.push(line);
149
+ lines.push('');
150
+ }
151
+ else {
152
+ lines.splice(extLineIdx + 1, 0, line);
153
+ }
154
+ await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`);
155
+ return true;
156
+ }
157
+ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterPath, label, options = {}) {
158
+ const normalizedPath = extensionPath.replace(/\\/g, '/');
159
+ const normalizedAfterPath = afterPath.replace(/\\/g, '/');
160
+ const line = ` - ${normalizedPath}`;
161
+ const raw = await readTextIfExists(configPath);
162
+ const lines = (raw || '').split('\n');
163
+ const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
164
+ const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
165
+ if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
166
+ return false;
167
+ if (options.dryRun) {
168
+ console.log(` [dry-run] would place ${label} after Ponytail in config.yml: ${normalizedPath}`);
169
+ return true;
170
+ }
171
+ if (existingIndex !== -1)
172
+ lines.splice(existingIndex, 1);
173
+ const refreshedAfterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
174
+ if (refreshedAfterIndex !== -1) {
175
+ lines.splice(refreshedAfterIndex + 1, 0, line);
176
+ }
177
+ else {
178
+ const extensionsIndex = lines.findIndex((entry) => EXTENSIONS_KEY_RE.test(entry));
179
+ if (extensionsIndex === -1) {
180
+ lines.push('extensions:', line, '');
181
+ }
182
+ else {
183
+ normalizeExtensionsKey(lines);
184
+ lines.splice(extensionsIndex + 1, 0, line);
185
+ }
186
+ }
187
+ await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`);
188
+ return true;
189
+ }
190
+ function readPonytailConfig() {
191
+ const dir = process.env.XDG_CONFIG_HOME
192
+ ? path.join(process.env.XDG_CONFIG_HOME, 'ponytail')
193
+ : path.join(HOME, '.config', 'ponytail');
194
+ return { dir, path: path.join(dir, 'config.json') };
195
+ }
196
+ function parseJsonObject(text) {
197
+ if (!text)
198
+ return null;
199
+ try {
200
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
201
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
202
+ return null;
203
+ return parsed;
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ function parsePonytailConfig(existing) {
210
+ return parseJsonObject(existing) ?? {};
211
+ }
212
+ async function patchPonytailConfig(apply, dryRunNote, writeNote, options = {}) {
213
+ const config = readPonytailConfig();
214
+ if (options.dryRun) {
215
+ console.log(` [dry-run] ${dryRunNote} in ${config.path}`);
216
+ return;
217
+ }
218
+ const cfg = parsePonytailConfig(await readTextIfExists(config.path));
219
+ if (!apply(cfg)) {
220
+ debug('Ponytail config already up to date');
221
+ return;
222
+ }
223
+ await fs.mkdir(config.dir, { recursive: true });
224
+ await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
225
+ console.log(` [write] ${writeNote} in ${config.path}`);
226
+ }
227
+ async function ensurePonytailConfigValue(key, value, options = {}) {
228
+ await patchPonytailConfig((cfg) => {
229
+ if (cfg[key] === value)
230
+ return false;
231
+ cfg[key] = value;
232
+ return true;
233
+ }, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
234
+ }
235
+ // Read plugins/package.json tolerantly; corrupt or missing files start fresh
236
+ // with installer-managed defaults.
237
+ async function readPluginsPackage(pkgPath) {
238
+ const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
239
+ return { ...pkg, name: pkg.name || 'omp-plugins', private: true, dependencies: pkg.dependencies || {} };
240
+ }
241
+ const SCOPE_MAP = { user: '1', project: '2', both: '3' };
242
+ function relTime(ageMs) {
243
+ const mins = Math.floor(ageMs / 60000);
244
+ if (mins < 1)
245
+ return 'just now';
246
+ if (mins < 60)
247
+ return `${mins} min${mins === 1 ? '' : 's'} ago`;
248
+ const hours = Math.floor(mins / 60);
249
+ if (hours < 24)
250
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
251
+ const days = Math.floor(hours / 24);
252
+ if (days < 14)
253
+ return `${days} day${days === 1 ? '' : 's'} ago`;
254
+ const weeks = Math.floor(days / 7);
255
+ return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
256
+ }
257
+ export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage, dashboard, dashboardPort, dashboardOpen, dashboardExport, removePonytail, keepPonytail, removeRtk, scopeFlag, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, SCOPE_MAP, relTime, };