@krtclcdy/tersio 2.15.0 → 2.16.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
@@ -10,24 +10,6 @@
10
10
 
11
11
  Terse replies, compact shell output, and minimal code decisions for [Oh My Pi (OMP)](https://github.com/can1357/oh-my-pi) — one-command combo presets.
12
12
 
13
- ## ✨ Features
14
-
15
- - **Caveman** — Shortens replies, keeps technical substance. Modes: `lite`, `full`, `ultra`, `wenyan`.
16
- - **RTK** — Routes noisy shell commands through the RTK binary for compact output; exact bytes bypass it by policy. The installer wires rtk into OMP automatically (`rtk init -g --agent omp`), so bash tool calls rewrite to `rtk` before execution and meter into the gain dashboard.
17
- - **Ponytail** — Minimal, YAGNI-oriented code decisions, from nudge (`lite`) to requirement-challenger (`ultra`).
18
- - **Combo** — Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max`.
19
- - **Updater** — `/tersio check` and `/tersio update` cover Ponytail, RTK, and Caveman in-session, with dry-run and backups.
20
- - **Gain dashboard** — `tersio gain` serves a local-only savings dashboard: cost with currency conversion, activity graph, command tools, and model breakdowns.
21
- - **Usage ledger** — `tersio usage` prints a ledger-backed savings report for the machine, local only.
22
-
23
- ## 🧱 Tech Stack
24
-
25
- - [TypeScript](https://www.typescriptlang.org/) + [Node.js](https://nodejs.org/): CLI and OMP extensions, compiled to dependency-free JS for install.
26
- - Vanilla dashboard: dependency-free HTML/CSS/JS export — no build step, no runtime deps.
27
- - [js-tiktoken](https://github.com/dqbd/tiktoken): Real `o200k_base` BPE token counts for benchmarks (dev only).
28
- - [tokscale](https://github.com/junhoyeo/tokscale): Token-bucket convention (input / output / cache read / cache write) for parsing OMP session transcripts.
29
- - [EcoLogits](https://github.com/genai-impact/ecologits): CO2 model (v0.8.2 port — per-model params, provider grids, served ÷32 amortization).
30
-
31
13
  ## ⚡ Getting Started
32
14
 
33
15
  **Default** (macOS/Linux/WSL) — one line. Installs the CLI via npm, then runs the main installer (scope + Combo preset menus) in the same pass:
package/cli/common.js CHANGED
@@ -104,7 +104,8 @@ async function writeIfChanged(dest, content, options = {}) {
104
104
  return false;
105
105
  }
106
106
  if (options.dryRun) {
107
- console.log(` [dry-run] would write ${dest}`);
107
+ if (verbose)
108
+ console.log(` [dry-run] would write ${dest}`);
108
109
  return true;
109
110
  }
110
111
  await fs.mkdir(path.dirname(dest), { recursive: true });
@@ -113,7 +114,8 @@ async function writeIfChanged(dest, content, options = {}) {
113
114
  debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
114
115
  }
115
116
  await fs.writeFile(dest, content, 'utf8');
116
- console.log(` [write] ${dest}`);
117
+ if (verbose && !options.quiet)
118
+ console.log(` [write] ${dest}`);
117
119
  return true;
118
120
  }
119
121
  // omp ships 'extensions: null'; appending list items under a null scalar breaks
@@ -127,11 +129,12 @@ function normalizeExtensionsKey(lines) {
127
129
  lines[idx] = 'extensions:';
128
130
  return true;
129
131
  }
130
- async function writeConfigLines(configPath, lines, logMsg) {
132
+ async function writeConfigLines(configPath, lines, logMsg, options = {}) {
131
133
  await fs.mkdir(path.dirname(configPath), { recursive: true });
132
134
  await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
133
135
  await fs.writeFile(configPath, lines.join('\n'), 'utf8');
134
- console.log(logMsg);
136
+ if (verbose && !options.quiet)
137
+ console.log(logMsg);
135
138
  }
136
139
  async function ensureExtensionInConfig(configPath, extensionPath, label, options = {}) {
137
140
  const normalizedPath = extensionPath.replace(/\\/g, '/');
@@ -144,7 +147,8 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
144
147
  }
145
148
  const extLineIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
146
149
  if (options.dryRun) {
147
- console.log(` [dry-run] would add ${label} to config.yml: ${normalizedPath}`);
150
+ if (verbose && !options.quiet)
151
+ console.log(` [dry-run] would add ${label} to config.yml`);
148
152
  return true;
149
153
  }
150
154
  if (normalizeExtensionsKey(lines)) {
@@ -158,7 +162,7 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
158
162
  else {
159
163
  lines.splice(extLineIdx + 1, 0, line);
160
164
  }
161
- await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`);
165
+ await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`, options);
162
166
  return true;
163
167
  }
164
168
  async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterPath, label, options = {}) {
@@ -172,7 +176,8 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
172
176
  if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
173
177
  return false;
174
178
  if (options.dryRun) {
175
- console.log(` [dry-run] would place ${label} after Ponytail in config.yml: ${normalizedPath}`);
179
+ if (verbose && !options.quiet)
180
+ console.log(` [dry-run] would place ${label} after Ponytail in config.yml`);
176
181
  return true;
177
182
  }
178
183
  if (existingIndex !== -1)
@@ -191,7 +196,7 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
191
196
  lines.splice(extensionsIndex + 1, 0, line);
192
197
  }
193
198
  }
194
- await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`);
199
+ await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`, options);
195
200
  return true;
196
201
  }
197
202
  function readPonytailConfig() {
@@ -219,7 +224,8 @@ function parsePonytailConfig(existing) {
219
224
  async function patchPonytailConfig(apply, dryRunNote, writeNote, options = {}) {
220
225
  const config = readPonytailConfig();
221
226
  if (options.dryRun) {
222
- console.log(` [dry-run] ${dryRunNote} in ${config.path}`);
227
+ if (verbose && !options.quiet)
228
+ console.log(` [dry-run] ${dryRunNote}`);
223
229
  return;
224
230
  }
225
231
  const cfg = parsePonytailConfig(await readTextIfExists(config.path));
@@ -229,7 +235,8 @@ async function patchPonytailConfig(apply, dryRunNote, writeNote, options = {}) {
229
235
  }
230
236
  await fs.mkdir(config.dir, { recursive: true });
231
237
  await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
232
- console.log(` [write] ${writeNote} in ${config.path}`);
238
+ if (verbose && !options.quiet)
239
+ console.log(` [write] ${writeNote}`);
233
240
  }
234
241
  async function ensurePonytailConfigValue(key, value, options = {}) {
235
242
  await patchPonytailConfig((cfg) => {
package/cli/doctor.js CHANGED
@@ -146,7 +146,10 @@ async function runDoctor() {
146
146
  check('RTK binary', rtkBinText !== null, rtkBinText === null ? rtkBin : [rtkVersion, rtkAge].filter(Boolean).join(', '));
147
147
  if (rtkBinText !== null && !rtkVersion)
148
148
  warnLine('RTK version', 'unavailable — binary may not be executable');
149
+ const rtkRegistered = rtkOmpText !== null && (configText ?? '').includes('extensions/rtk.ts');
149
150
  check('RTK OMP wiring (rtk.ts)', rtkOmpText !== null, rtkOmpText === null ? 'run: rtk init -g --agent omp' : '');
151
+ if (rtkOmpText !== null && !rtkRegistered)
152
+ warnLine('RTK in config.yml', 'rtk.ts not listed — OMP will not load it; rerun install');
150
153
  const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
151
154
  check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
152
155
  const total = tally.ok + tally.missing + tally.warn;
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, 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';
6
+ import { BUN_BIN_DIR, CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, 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';
@@ -32,17 +32,18 @@ const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.js');
32
32
  const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
33
33
  const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
34
34
  async function stepPonytail(pluginsDir, userDir, options) {
35
- console.log('\n[1/8] Installing Ponytail plugin...');
35
+ if (!options.quiet)
36
+ console.log(' Ponytail — refresh plugin');
36
37
  await fs.mkdir(pluginsDir, { recursive: true });
37
38
  const pkgPath = path.join(pluginsDir, 'package.json');
38
39
  const pkg = await readPluginsPackage(pkgPath);
39
40
  pkg.dependencies['@dietrichgebert/ponytail'] = PONYTAIL_GITHUB_SPEC;
40
41
  if (options.dryRun) {
41
- console.log(` [dry-run] would write ${pkgPath}`);
42
+ if (verbose && !options.quiet)
43
+ console.log(` [dry-run] would write ${pkgPath}`);
42
44
  }
43
45
  else {
44
46
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
45
- console.log(' [write] package.json');
46
47
  }
47
48
  const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
48
49
  const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
@@ -53,8 +54,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
53
54
  }
54
55
  else if (options.dryRun) {
55
56
  // Dry runs preview the wiring below without touching the network.
56
- console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
57
- if (options.reinstall) {
57
+ if (verbose && !options.quiet)
58
+ console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
59
+ if (options.reinstall && verbose && !options.quiet) {
58
60
  console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
59
61
  }
60
62
  ponytailExtExists = true;
@@ -63,7 +65,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
63
65
  // Try omp plugin install first
64
66
  try {
65
67
  await execNetwork('Installing Ponytail plugin', OMP_BIN, ['plugin', 'install', PONYTAIL_GITHUB_SPEC], { cwd: pluginsDir });
66
- console.log(' [ok] omp plugin install ran');
67
68
  }
68
69
  catch (e) {
69
70
  console.log(` [warn] omp plugin install failed: ${e.message}`);
@@ -71,7 +72,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
71
72
  if (options.reinstall) {
72
73
  try {
73
74
  await execNetwork('Refreshing Ponytail package', 'npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 });
74
- console.log(' [ok] Ponytail refreshed');
75
75
  }
76
76
  catch (e) {
77
77
  console.log(` [fail] Could not refresh ponytail: ${e.message}`);
@@ -82,15 +82,14 @@ async function stepPonytail(pluginsDir, userDir, options) {
82
82
  ponytailExtExists = await probeExt();
83
83
  // Fallback: try bun install or npm install
84
84
  if (!ponytailExtExists) {
85
- console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
85
+ if (!options.quiet)
86
+ console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
86
87
  try {
87
88
  await execNetwork('Installing Ponytail dependencies', 'npm', ['install'], { cwd: pluginsDir, timeout: 120000 });
88
- console.log(' [ok] npm install completed');
89
89
  }
90
90
  catch {
91
91
  try {
92
92
  await execNetwork('Installing Ponytail dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 120000 });
93
- console.log(' [ok] bun install completed');
94
93
  }
95
94
  catch (e2) {
96
95
  console.log(` [fail] Could not install ponytail: ${e2.message}`);
@@ -101,12 +100,12 @@ async function stepPonytail(pluginsDir, userDir, options) {
101
100
  }
102
101
  // Last-resort fallback: git clone the repo into node_modules
103
102
  if (!ponytailExtExists) {
104
- console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
103
+ if (!options.quiet)
104
+ console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
105
105
  try {
106
106
  const dest = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
107
107
  await fs.mkdir(path.dirname(dest), { recursive: true });
108
108
  await execNetwork('Cloning Ponytail repository', 'git', ['clone', '--depth', '1', 'https://github.com/DietrichGebert/ponytail.git', dest], { timeout: 180000 });
109
- console.log(' [ok] git clone completed');
110
109
  ponytailExtExists = await probeExt();
111
110
  }
112
111
  catch (e3) {
@@ -119,9 +118,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
119
118
  console.log(' [skip] Ponytail pi-extension/index.js still not found — skill-only mode');
120
119
  console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
121
120
  }
122
- else if (!options.dryRun) {
121
+ else if (!options.dryRun && !options.quiet) {
123
122
  // Wire extension into config.yml so /ponytail command loads
124
- console.log(' [ok] Ponytail pi-extension found');
123
+ debug('Ponytail pi-extension found');
125
124
  }
126
125
  // Still set Ponytail config defaults even without the extension command
127
126
  const configPath = path.join(userDir, 'config.yml');
@@ -134,20 +133,23 @@ async function stepPonytail(pluginsDir, userDir, options) {
134
133
  // Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
135
134
  // Returns true when the package is verified in plugins/node_modules.
136
135
  async function stepSelfPlugin(pluginsDir, options) {
137
- console.log('\n[2/8] Registering tersio as OMP plugin...');
136
+ if (!options.quiet)
137
+ console.log(' Tersio — register plugin');
138
138
  const pkgPath = path.join(pluginsDir, 'package.json');
139
139
  const pkg = await readPluginsPackage(pkgPath);
140
140
  pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
141
141
  for (const legacy of ['oh-my-pi-token-saver', 'tersio-omp']) {
142
142
  if (legacy in pkg.dependencies) {
143
143
  delete pkg.dependencies[legacy];
144
- if (!options.dryRun)
144
+ if (!options.dryRun && !options.quiet)
145
145
  console.log(` [migrate] dropped legacy ${legacy} dependency`);
146
146
  }
147
147
  }
148
148
  if (options.dryRun) {
149
- console.log(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
150
- console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
149
+ if (verbose && !options.quiet)
150
+ console.log(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
151
+ if (verbose && !options.quiet)
152
+ console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
151
153
  return false;
152
154
  }
153
155
  // pkg.dependencies[PACKAGE_NAME] was assigned above, so only reinstall skips this fast path.
@@ -155,7 +157,6 @@ async function stepSelfPlugin(pluginsDir, options) {
155
157
  const installedPkgRaw = await readTextIfExists(path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json'));
156
158
  if (parseJsonObject(installedPkgRaw)?.version === PACKAGE_VERSION) {
157
159
  debug(`${PACKAGE_NAME} already installed at v${PACKAGE_VERSION}; skipping npm install`);
158
- console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
159
160
  return true;
160
161
  }
161
162
  }
@@ -179,7 +180,7 @@ async function stepSelfPlugin(pluginsDir, options) {
179
180
  console.log(` [warn] ${PACKAGE_NAME} not found in plugins/node_modules after install`);
180
181
  return false;
181
182
  }
182
- console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
183
+ debug('tersio listed in OMP plugins');
183
184
  return true;
184
185
  }
185
186
  function resolveRtkTriple() {
@@ -211,19 +212,21 @@ async function downloadRtkChecksums(release) {
211
212
  return null;
212
213
  }
213
214
  }
214
- async function verifyRtkArchive(archivePath, assetName, checksumsText) {
215
+ async function verifyRtkArchive(archivePath, assetName, checksumsText, options = {}) {
215
216
  if (!checksumsText) {
216
- console.log(' [warn] No checksums.txt available — skipping verification');
217
+ if (!options.quiet)
218
+ console.log(' [warn] No checksums.txt available — skipping verification');
217
219
  return true;
218
220
  }
219
221
  const expected = parseChecksum(checksumsText, assetName);
220
222
  const actual = await sha256File(archivePath);
221
223
  if (!expected) {
222
- console.log(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
224
+ if (!options.quiet)
225
+ console.log(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
223
226
  return true;
224
227
  }
225
228
  if (actual === expected) {
226
- console.log(` [ok] Checksum verified for ${assetName}`);
229
+ debug(`Checksum verified for ${assetName}`);
227
230
  return true;
228
231
  }
229
232
  console.log(` [fail] Checksum mismatch for ${assetName}`);
@@ -272,10 +275,18 @@ async function extractRtkArchive(archivePath, extractDir) {
272
275
  return false;
273
276
  }
274
277
  async function stepRtk(binDir, options) {
275
- console.log('\n[3/8] Installing RTK binary...');
278
+ if (!options.quiet)
279
+ console.log(' RTK — download binary and wire into OMP');
276
280
  const binDest = path.join(binDir, RTK_BINARY_NAME);
277
281
  // Download failure must not skip wiring: a pre-existing rtk binary is
278
282
  // exactly as good for the OMP hook, so wire whatever ends up at binDest.
283
+ // Dry runs stay offline: no registry probe, just the plan line above.
284
+ if (options.dryRun) {
285
+ if (verbose && !options.quiet)
286
+ console.log(` [dry-run] would download rtk binary and install to ${binDest}`);
287
+ await wireRtkOmp(binDest, options);
288
+ return;
289
+ }
279
290
  try {
280
291
  const release = await withInteractiveSpinner('Finding latest RTK release', () => fetchJson(RTK_RELEASE_API));
281
292
  const triple = resolveRtkTriple();
@@ -284,12 +295,6 @@ async function stepRtk(binDir, options) {
284
295
  const asset = findRtkAsset(release, triple);
285
296
  if (!asset)
286
297
  return;
287
- if (options.dryRun) {
288
- console.log(` [dry-run] would download ${asset.name} from release ${release.tag_name}`);
289
- console.log(' [dry-run] would verify checksum against checksums.txt');
290
- console.log(` [dry-run] would extract and install to ${binDest}`);
291
- return;
292
- }
293
298
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'omp-rtk-'));
294
299
  try {
295
300
  const archivePath = path.join(tmpDir, asset.name);
@@ -301,7 +306,7 @@ async function stepRtk(binDir, options) {
301
306
  update('Verifying RTK checksum');
302
307
  return downloadedChecksums;
303
308
  });
304
- if (!await verifyRtkArchive(archivePath, asset.name, checksumsText))
309
+ if (!await verifyRtkArchive(archivePath, asset.name, checksumsText, options))
305
310
  return;
306
311
  const extractDir = path.join(tmpDir, 'extracted');
307
312
  if (!await extractRtkArchive(archivePath, extractDir))
@@ -314,14 +319,12 @@ async function stepRtk(binDir, options) {
314
319
  await fs.mkdir(path.dirname(binDest), { recursive: true });
315
320
  await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
316
321
  await fs.copyFile(found, binDest);
317
- console.log(` [write] ${binDest}`);
318
322
  if (!IS_WINDOWS) {
319
323
  await fs.chmod(binDest, 0o755);
320
324
  debug(`chmod 755 ${binDest}`);
321
325
  }
322
326
  try {
323
- const v = (await execP(binDest, ['--version'], { timeout: 30000, shell: false })).stdout.trim();
324
- console.log(` [ok] ${binDest} → ${v}`);
327
+ await execP(binDest, ['--version'], { timeout: 30000, shell: false });
325
328
  }
326
329
  catch {
327
330
  console.log(` [hint] Verify manually: ${binDest} --version`);
@@ -352,7 +355,8 @@ async function stepRtk(binDir, options) {
352
355
  async function copySources(extDir, files, skipLabel, options) {
353
356
  const src = await readTextIfExists(files[0][0]);
354
357
  if (!src) {
355
- console.log(` [skip] ${skipLabel} not found in repo`);
358
+ if (!options.quiet && options.dryRun)
359
+ console.log(` [skip] ${skipLabel} not found in repo`);
356
360
  return false;
357
361
  }
358
362
  await writeIfChanged(path.join(extDir, files[0][1]), src, options);
@@ -366,6 +370,8 @@ async function copySources(extDir, files, skipLabel, options) {
366
370
  return true;
367
371
  }
368
372
  async function stepSharedSessionState(extDir, options) {
373
+ if (!options.quiet)
374
+ console.log(' Shared files — sync session bridge');
369
375
  await copySources(extDir, [
370
376
  [SHARED_SESSION_STATE, path.join('shared', 'session-state.js')],
371
377
  [SHARED_TYPES, path.join('shared', 'types.js')],
@@ -377,14 +383,16 @@ async function stepSharedSessionState(extDir, options) {
377
383
  ], 'shared/session-state.js', options);
378
384
  }
379
385
  async function stepModeReinforcement(extDir, ponytailExtPath, options) {
380
- console.log('\n[8/8] Installing mode reinforcement extension...');
386
+ if (!options.quiet)
387
+ console.log(' Session helpers — sync shared files');
381
388
  const dest = path.join(extDir, 'shared', 'mode-reinforcement.js');
382
389
  if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.js')]], 'shared/mode-reinforcement.js', options))
383
390
  return;
384
391
  await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
385
392
  }
386
393
  async function stepRtkSession(extDir, options) {
387
- console.log('\n[4/8] Installing RTK session extension...');
394
+ if (!options.quiet)
395
+ console.log(' RTK session — install session mode');
388
396
  await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.js')]], 'rtk-session/index.js', options);
389
397
  }
390
398
  // One rule fetch serves the install; dry runs stay offline
@@ -401,13 +409,14 @@ async function fetchCavemanRule(options) {
401
409
  }
402
410
  }
403
411
  async function stepCaveman(extDir, rule, options) {
404
- console.log('\n[5/8] Installing Caveman session extension...');
412
+ if (!options.quiet)
413
+ console.log(' Caveman — fetch rule and install session mode');
405
414
  const cavemanDir = path.join(extDir, 'caveman-session');
406
415
  if (!options.dryRun)
407
416
  await fs.mkdir(cavemanDir, { recursive: true });
408
417
  const ruleDest = path.join(cavemanDir, 'rule.md');
409
418
  if (rule === null && (await readTextIfExists(ruleDest)) !== null) {
410
- console.log(' [info] Keeping existing rule.md');
419
+ debug('Keeping existing rule.md');
411
420
  }
412
421
  else if (rule === null) {
413
422
  console.log(' [skip] Caveman rule.md unavailable');
@@ -419,14 +428,16 @@ async function stepCaveman(extDir, rule, options) {
419
428
  await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.js')]], 'caveman-session/index.js', options);
420
429
  }
421
430
  async function stepTersioCommands(extDir, options) {
422
- console.log('\n[7/8] Installing Tersio root-command extension...');
431
+ if (!options.quiet)
432
+ console.log(' Tersio commands — install /tersio root command');
423
433
  await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.js')]], 'tersio-commands/index.js', options);
424
434
  }
425
435
  async function stepUpdater(extDir, options) {
426
436
  await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.js')]], 'ai-addons-updater/index.js', options);
427
437
  }
428
438
  async function stepCombo(extDir, options) {
429
- console.log('\n[6/8] Installing Combo toggle extension...');
439
+ if (!options.quiet)
440
+ console.log(' Combo — install preset switch');
430
441
  const dest = path.join(extDir, 'combo-toggle', 'index.js');
431
442
  if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.js')]], 'combo-toggle/index.js', options))
432
443
  return;
@@ -473,7 +484,7 @@ async function storedProfile() {
473
484
  base.ponytailDefault = stored.ponytailDefault;
474
485
  return base;
475
486
  }
476
- async function resolveProfile(forceReinstall = false) {
487
+ async function resolveProfile(forceReinstall = false, opts = {}) {
477
488
  // Seed from the lock file so flag-less update/reinstall runs keep the
478
489
  // user's configured defaults instead of resetting them to off.
479
490
  const profile = await storedProfile();
@@ -508,32 +519,10 @@ async function resolveProfile(forceReinstall = false) {
508
519
  profile.rtkDefault = rtkDefaultFlag === 'on';
509
520
  if (ponytailDefaultFlag !== undefined)
510
521
  profile.ponytailDefault = ponytailDefaultFlag;
511
- console.log(` Profile: combo default=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
522
+ if ((!opts.quiet && verbose) || dryRun)
523
+ console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
512
524
  return profile;
513
525
  }
514
- // ponytail: post-install guard. List entries under `extensions: null` break
515
- // OMP launch, so fail loudly here instead of at the next OMP start.
516
- async function validateConfigExtensions(agentDir, options) {
517
- if (options.dryRun)
518
- return;
519
- const raw = await readTextIfExists(path.join(agentDir, 'config.yml'));
520
- if (!raw)
521
- return;
522
- const lines = raw.split('\n');
523
- const keyIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
524
- if (keyIdx === -1)
525
- return;
526
- const scalarNull = /^\s*extensions\s*:\s*(null|~|\[\s*\])\s*$/i.test(lines[keyIdx]);
527
- const hasEntries = lines.slice(keyIdx + 1).some((l) => /^\s*-\s+\S/.test(l));
528
- if (scalarNull && hasEntries) {
529
- console.log(' [fail] config.yml lists extensions under `extensions: null` — OMP will fail to launch.');
530
- console.log(' [hint] Replace the `extensions: null` line with `extensions:`, then reinstall.');
531
- process.exitCode = 1;
532
- }
533
- else {
534
- console.log(' [ok] config.yml extensions key valid');
535
- }
536
- }
537
526
  // Persist the profile as omp plugin settings so `omp plugin config get`
538
527
  // reflects the choice and the extensions pick it up on session start.
539
528
  async function writePluginSettings(profile, options) {
@@ -559,7 +548,8 @@ async function writePluginSettings(profile, options) {
559
548
  ponytailDefault: profile.ponytailDefault,
560
549
  };
561
550
  if (options.dryRun) {
562
- console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
551
+ if (verbose && !options.quiet)
552
+ console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
563
553
  return;
564
554
  }
565
555
  await fs.mkdir(pluginsDir, { recursive: true });
@@ -606,16 +596,13 @@ async function runCommandMenu() {
606
596
  await runInstall();
607
597
  break;
608
598
  case 'update': {
609
- // One bound flow: report the CLI version, then offer the add-on
610
- // refresh in the same pass — no redundant second "update" option.
611
- const latest = await checkForUpdate(true);
612
- if (typeof latest === 'string')
613
- console.log(` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})`);
614
- else if (latest === 'unknown')
615
- console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
599
+ // One bound flow: the version check already ran above, so report it
600
+ // and offer the refresh in the same breath — no second "update" quiz.
601
+ if (typeof newer === 'string')
602
+ console.log(` tersio ${newer} available (installed ${PACKAGE_VERSION})`);
616
603
  else
617
- console.log(` [ok] tersio ${PACKAGE_VERSION} is the latest`);
618
- const go = await askInteractiveConfirm('Update add-ons now (RTK, Caveman rule, Ponytail)?');
604
+ console.log(` tersio ${PACKAGE_VERSION} is the latest`);
605
+ const go = await askInteractiveConfirm('Update now (CLI + RTK, Caveman rule, Ponytail)?');
619
606
  if (go.status === 'confirmed' && go.value) {
620
607
  await runLatestUpdate();
621
608
  }
@@ -664,19 +651,23 @@ async function runInstall(overrides = {}) {
664
651
  await runCommandMenu();
665
652
  return;
666
653
  }
667
- printWelcome();
654
+ // apply-update is the delegated payload of `tersio update`: stay silent —
655
+ // the parent already printed the plan and owns the closing summary. Banner
656
+ // repeats here otherwise (the reported double-print), burying real output.
657
+ const quiet = applyUpdate;
658
+ if (!quiet)
659
+ printWelcome();
668
660
  if (isReinstall) {
669
661
  // removeRtk stays false: reinstall is about to replace the binary, and
670
662
  // deleting it first would leave nothing to wire if the fresh download
671
663
  // fails (rate limit, offline). rtk.ts is removed here and re-wired below.
672
664
  await runUninstall({ yes: true, removePonytail: false, removeRtk: false });
673
665
  }
674
- if (dryRun)
675
- console.log('[dry-run] No changes will be written.\n');
676
- console.log(`=== Tersio v${PACKAGE_VERSION} ===`);
677
- console.log(` Platform: ${process.platform}`);
678
- console.log(` Arch: ${process.arch}`);
679
- console.log(` Home: ${HOME}`);
666
+ if (dryRun && !quiet)
667
+ console.log('Preview — no changes will be written.\n');
668
+ if (!quiet) {
669
+ console.log(`Installing Tersio v${PACKAGE_VERSION}`);
670
+ }
680
671
  // Remind humans a newer release exists; silent for scripts (no TTY) and for
681
672
  // the apply-update payload, which is itself an update run.
682
673
  if (tty() && !applyUpdate && command !== 'uninstall') {
@@ -703,47 +694,54 @@ async function runInstall(overrides = {}) {
703
694
  }
704
695
  }
705
696
  }
706
- console.log(' Scope: user (all OMP sessions)');
707
697
  // Resolve session defaults: flags > interactive prompt > defaults.
708
- const profile = await resolveProfile(isReinstall);
698
+ const profile = await resolveProfile(isReinstall, { quiet });
709
699
  const userDir = OMP_AGENT_DIR;
710
700
  const userExtDir = path.join(userDir, 'extensions');
711
701
  // apply-update is `tersio update`'s payload run: treat it like reinstall so
712
702
  // the add-ons refresh too — Ponytail package via npm, self plugin, RTK
713
703
  // binary (always re-downloaded), and the Caveman rule (always re-fetched).
714
- const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate };
715
- // Check prerequisites
716
- console.log('\nPrerequisites:');
704
+ const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate, quiet };
717
705
  try {
718
706
  const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
719
- console.log(` [ok] omp ${v}`);
707
+ if (verbose && !quiet)
708
+ console.log(` omp ${v}`);
720
709
  }
721
710
  catch {
722
711
  console.log(' [fail] omp not found — ensure it\'s installed');
723
712
  }
724
713
  const cavemanRule = await fetchCavemanRule(options);
725
- console.log('\n--- User-level install ---');
726
- await stepSharedSessionState(userExtDir, options);
727
- await stepPonytail(OMP_PLUGINS_DIR, userDir, options);
728
- const selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options);
714
+ const failures = [];
715
+ const capture = async (label, work) => {
716
+ try {
717
+ await work();
718
+ }
719
+ catch (e) {
720
+ failures.push(label);
721
+ console.log(` [fail] ${label}: ${shortError(e)}`);
722
+ }
723
+ };
724
+ await capture('shared', () => stepSharedSessionState(userExtDir, options));
725
+ await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
726
+ let selfPlugin = false;
727
+ await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options); });
729
728
  const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
730
- await stepRtk(BUN_BIN_DIR, options);
731
- await stepRtkSession(userExtDir, options);
732
- await stepCaveman(userExtDir, cavemanRule, options);
733
- await stepCombo(userExtDir, options);
734
- await stepTersioCommands(userExtDir, options);
735
- await stepModeReinforcement(userExtDir, ponytailExtPath, options);
736
- await stepUpdater(userExtDir, options);
729
+ await capture('rtk', () => stepRtk(BUN_BIN_DIR, options));
730
+ await capture('rtk session', () => stepRtkSession(userExtDir, options));
731
+ await capture('caveman', () => stepCaveman(userExtDir, cavemanRule, options));
732
+ await capture('combo', () => stepCombo(userExtDir, options));
733
+ await capture('commands', () => stepTersioCommands(userExtDir, options));
734
+ await capture('reinforcement', () => stepModeReinforcement(userExtDir, ponytailExtPath, options));
735
+ await capture('updater', () => stepUpdater(userExtDir, options));
737
736
  if (selfPlugin)
738
- await writePluginSettings(profile, options);
739
- console.log('\n=== Installation complete ===');
740
- console.log('\nNext steps:');
741
- console.log(' 1. Restart OMP');
742
- console.log(' 2. /caveman full');
743
- console.log(' 3. /rtk on');
744
- console.log(' 4. /ponytail full');
745
- console.log(' 5. /ai-addons check');
746
- console.log(' 6. /combo medium (toggle all 3 at once — off by default)');
737
+ await capture('settings', () => writePluginSettings(profile, options));
738
+ if (quiet) {
739
+ if (failures.length > 0)
740
+ console.log(` add-ons: ${failures.length} failed (${failures.join(', ')}) — see [fail] lines above`);
741
+ }
742
+ else {
743
+ console.log(failures.length === 0 ? '\nDone — restart OMP, then /combo medium.' : `\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
744
+ }
747
745
  closeRL();
748
746
  }
749
747
  export { runInstall };
package/cli/rtk-wiring.js CHANGED
@@ -1,12 +1,20 @@
1
1
  // cli/rtk-wiring.ts — wire the installed rtk binary into OMP.
2
2
  // `rtk init -g --agent omp` writes rtk's tool_call extension to
3
- // ~/.omp/agent/extensions/rtk.ts; OMP auto-loads it and rewrites bash
4
- // commands to rtk before execution, so every rewritten call lands in
5
- // rtk's history.db and shows metered savings in the gain dashboard.
3
+ // ~/.omp/agent/extensions/rtk.ts, and we register that path in config.yml
4
+ // (OMP only loads listed extensions — rtk init does not register it).
5
+ // Once loaded, OMP rewrites bash commands to rtk before execution, so every
6
+ // rewritten call lands in rtk's history.db and shows metered savings.
6
7
  // Fail-open: a failed wire only downgrades to manual `rtk` prefixing.
7
8
  // Deliberately free of cli/common.ts imports (argv side effects) so tests
8
9
  // can load it directly.
10
+ import { promises as fs } from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
9
13
  import { execFile } from 'node:child_process';
14
+ function debugWire(options, msg) {
15
+ if (!options.quiet)
16
+ console.log(` ${msg}`);
17
+ }
10
18
  function shortWiringError(e) {
11
19
  const err = e;
12
20
  const why = [err.code !== undefined ? `code ${err.code}` : '', err.signal ?? '', err.stderr?.trim(), err.message]
@@ -29,14 +37,64 @@ function execFileP(cmd, args, timeout) {
29
37
  // Idempotent: rtk rewrites its extension file on every init run, so
30
38
  // reinstalling tersio refreshes the wiring for free.
31
39
  export async function wireRtkOmp(rtkBin, options = {}) {
32
- console.log(' Wiring rtk → OMP (bash tool_call rewrite)...');
33
- if (options.dryRun) {
34
- console.log(' [dry-run] would run: rtk init -g --agent omp');
40
+ if (!options.dryRun)
41
+ debugWire(options, 'Wiring rtk → OMP (bash tool_call rewrite)…');
42
+ if (options.dryRun)
35
43
  return true;
44
+ const wired = await runRtkInit(rtkBin, options);
45
+ if (!wired)
46
+ return false;
47
+ await ensureRtkInConfig(options);
48
+ return true;
49
+ }
50
+ // The rtk.ts path rtk init writes (mirrors OMP_AGENT_DIR in cli/common.ts
51
+ // without importing it — see header note on argv side effects).
52
+ function rtkExtensionPath() {
53
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
54
+ return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
55
+ }
56
+ // OMP loads only listed extensions: append rtk.ts after the existing entries
57
+ // (or create the key) so a fresh wire takes effect on next OMP start.
58
+ async function ensureRtkInConfig(options) {
59
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
60
+ const configPath = path.join(home, '.omp', 'agent', 'config.yml');
61
+ const extPath = rtkExtensionPath().replace(/\\/g, '/');
62
+ let raw = null;
63
+ try {
64
+ raw = await fs.readFile(configPath, 'utf8');
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ if (raw.includes(extPath)) {
70
+ debugWire(options, 'rtk already in config.yml');
71
+ return;
36
72
  }
73
+ const lines = raw.split('\n');
74
+ const keyIdx = lines.findIndex((l) => /^\s*extensions\s*:/i.test(l));
75
+ const line = ` - ${extPath}`;
76
+ if (keyIdx === -1) {
77
+ lines.push('extensions:', line, '');
78
+ }
79
+ else {
80
+ if (/^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i.test(lines[keyIdx]))
81
+ lines[keyIdx] = 'extensions:';
82
+ lines.splice(keyIdx + 1, 0, line);
83
+ }
84
+ try {
85
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
86
+ await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
87
+ await fs.writeFile(configPath, lines.join('\n'), 'utf8');
88
+ debugWire(options, 'rtk registered in config.yml');
89
+ }
90
+ catch (e) {
91
+ console.log(` [warn] could not register rtk in config.yml: ${e.message}`);
92
+ }
93
+ }
94
+ async function runRtkInit(rtkBin, options) {
37
95
  try {
38
96
  await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
39
- console.log(' [ok] rtk OMP extension wired (~/.omp/agent/extensions/rtk.ts)');
97
+ debugWire(options, 'rtk OMP extension wired');
40
98
  return true;
41
99
  }
42
100
  catch (first) {
@@ -45,7 +103,7 @@ export async function wireRtkOmp(rtkBin, options = {}) {
45
103
  await new Promise((resolve) => setTimeout(resolve, 2000));
46
104
  try {
47
105
  await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
48
- console.log(' [ok] rtk OMP extension wired (~/.omp/agent/extensions/rtk.ts)');
106
+ debugWire(options, 'rtk OMP extension wired');
49
107
  return true;
50
108
  }
51
109
  catch (e) {
package/cli/update.js CHANGED
@@ -116,15 +116,28 @@ async function probeUpdatePlan(cliLatest) {
116
116
  ponytail: [ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null, ponytailLatest],
117
117
  };
118
118
  }
119
- function planLine(name, current, latest) {
120
- if (!current && !latest)
121
- console.log(` ${name}: unknown`);
122
- else if (!latest)
123
- console.log(` ${name}: ${current} (latest unknown)`);
124
- else if (current === latest)
125
- console.log(` ${name}: ${current} (up to date)`);
126
- else
127
- console.log(` ${name}: ${current ?? 'missing'} → ${latest}`);
119
+ // Every add-on prints its status: version jump when stale, "up to date"
120
+ // otherwise, honest "unknown" when the probe cannot reach its source.
121
+ function planLines(plan) {
122
+ const stale = [];
123
+ const status = [];
124
+ const push = (name, current, latest) => {
125
+ if (!current && !latest)
126
+ status.push(`${name}: unknown`);
127
+ else if (!latest)
128
+ status.push(`${name}: ${current} (latest unknown)`);
129
+ else if (current === latest)
130
+ status.push(`${name}: ${current} (up to date)`);
131
+ else {
132
+ status.push(`${name}: ${current ?? 'missing'} → ${latest}`);
133
+ stale.push(`${name}: ${current ?? 'missing'} → ${latest}`);
134
+ }
135
+ };
136
+ push('Tersio', PACKAGE_VERSION, plan.cli);
137
+ push('RTK', plan.rtk[0], plan.rtk[1]);
138
+ push('Caveman rule', plan.rule[0], plan.rule[1]);
139
+ push('Ponytail', plan.ponytail[0], plan.ponytail[1]);
140
+ return { stale, status };
128
141
  }
129
142
  async function runLatestUpdate() {
130
143
  const forwardedArgs = ['--yes'];
@@ -133,7 +146,6 @@ async function runLatestUpdate() {
133
146
  if (verbose)
134
147
  forwardedArgs.push('--verbose');
135
148
  const npmCommand = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
136
- console.log('=== Updating Tersio ===');
137
149
  // Resolve the target explicitly: `npm view --prefer-online` beats the local
138
150
  // metadata cache, so a stale `@latest` can never pin an older release.
139
151
  const cliLatest = await latestPublishedVersion();
@@ -141,37 +153,38 @@ async function runLatestUpdate() {
141
153
  // Current → latest per add-on. Best-effort; unreachable probes print as
142
154
  // unknown and never block the update.
143
155
  const plan = await probeUpdatePlan(cliLatest);
144
- planLine('Tersio', PACKAGE_VERSION, plan.cli);
145
- planLine('RTK', plan.rtk[0], plan.rtk[1]);
146
- planLine('Caveman rule', plan.rule[0], plan.rule[1]);
147
- planLine('Ponytail', plan.ponytail[0], plan.ponytail[1]);
148
- console.log('');
156
+ const { stale, status } = planLines(plan);
157
+ if (dryRun) {
158
+ console.log(`tersio update (dry-run):\n ${status.join('\n ')}`);
159
+ console.log(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
160
+ console.log(` [dry-run] would delegate: npm exec --yes --prefer-online --package=${PACKAGE_NAME}${target} -- tersio --apply-update ${forwardedArgs.join(' ')}`);
161
+ return;
162
+ }
163
+ console.log(`Checking for updates:\n ${status.join('\n ')}`);
164
+ if (stale.length === 0) {
165
+ console.log(`tersio ${PACKAGE_VERSION} — up to date`);
166
+ return;
167
+ }
168
+ console.log(`Updating ${stale.join(', ')}`);
149
169
  // The npx delegation below only refreshes the OMP-side files; the globally
150
170
  // installed CLI keeps its old version until npm -g runs. Refresh both.
151
171
  const globalArgs = IS_WINDOWS
152
172
  ? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online']
153
173
  : ['install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online'];
154
- if (dryRun) {
155
- console.log(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
174
+ try {
175
+ const globalResult = await execNetwork(`Updating global Tersio CLI to ${PACKAGE_NAME}${target}`, npmCommand, globalArgs, {
176
+ timeout: 300000,
177
+ maxBuffer: 10 * 1024 * 1024,
178
+ windowsHide: true,
179
+ shell: false,
180
+ });
181
+ // npm -g echoes its own added/changed summary; only failures surface here.
182
+ if (globalResult.stderr)
183
+ process.stderr.write(globalResult.stderr);
156
184
  }
157
- else {
158
- try {
159
- const globalResult = await execNetwork(`Updating global Tersio CLI to ${PACKAGE_NAME}${target}`, npmCommand, globalArgs, {
160
- timeout: 300000,
161
- maxBuffer: 10 * 1024 * 1024,
162
- windowsHide: true,
163
- shell: false,
164
- });
165
- if (globalResult.stdout)
166
- process.stdout.write(globalResult.stdout);
167
- if (globalResult.stderr)
168
- process.stderr.write(globalResult.stderr);
169
- console.log(` [ok] CLI updated to ${PACKAGE_NAME}${target}`);
170
- }
171
- catch (e) {
172
- console.log(` [warn] Global CLI update skipped: ${e.message}`);
173
- console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
174
- }
185
+ catch (e) {
186
+ console.log(` [warn] Global CLI update skipped: ${e.message}`);
187
+ console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
175
188
  }
176
189
  const npmArgs = [
177
190
  'exec',
@@ -184,23 +197,22 @@ async function runLatestUpdate() {
184
197
  ...forwardedArgs,
185
198
  ];
186
199
  const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
187
- // Inherited stdio, no outer spinner: the delegated installer renders its
188
- // own Clack UI live, which capture-then-dump would corrupt into stray bars.
189
- console.log(` Running ${PACKAGE_NAME}${target} installer...`);
200
+ // Inherited stdio, no outer spinner. The delegated installer runs quiet
201
+ // (banner and per-file writes suppressed); this parent owns both the plan
202
+ // line above and the closing summary below — one voice, no repeats.
190
203
  try {
191
204
  await execInherit(npmCommand, npmCommandArgs);
192
- console.log('\n=== Update complete ===');
205
+ console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart OMP.`);
193
206
  if (!dryRun) {
194
207
  try {
195
- if (await refreshPrices())
196
- console.log(' [ok] model price table refreshed');
208
+ await refreshPrices();
197
209
  }
198
210
  catch { /* pricing is best-effort; update already succeeded */ }
199
211
  }
200
212
  }
201
213
  catch (e) {
202
214
  const err = e;
203
- console.error(`\n[fail] Could not run ${PACKAGE_NAME}${target}: ${err.message}`);
215
+ console.error(`[fail] Could not run ${PACKAGE_NAME}${target}: ${err.message}`);
204
216
  process.exitCode = 1;
205
217
  }
206
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "✂️ Token-saving OMP add-ons: terse Caveman replies, compact RTK shell output, lean Ponytail code calls — one-command combo presets.",
5
5
  "keywords": [
6
6
  "omp",