@krtclcdy/tersio 2.21.0 → 2.22.1

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.
@@ -12,7 +12,6 @@ import { CAVEMAN_REMOTE_RULE as CAVEMAN_REMOTE, RTK_RELEASE_API, fetchJson, find
12
12
  const IS_WINDOWS = process.platform === 'win32';
13
13
  const HOME = os.homedir();
14
14
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
15
- const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
16
15
  const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
17
16
  const RTK_BINARY = path.join(HOME, '.bun', 'bin', IS_WINDOWS ? 'rtk.exe' : 'rtk');
18
17
  const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'agent', 'extensions', 'caveman-session', 'rule.md');
@@ -119,38 +118,22 @@ async function checkAddons(ctx) {
119
118
  return lines.join('\n');
120
119
  }
121
120
  async function updatePonytail(pi, ctx, dryRun = false) {
122
- const pluginsDir = path.join(HOME, '.omp', 'plugins');
123
- if (dryRun) {
124
- const m = `Ponytail dry-run: would run \`npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund\` in ${pluginsDir}.`;
125
- return report(ctx, m, 'info');
126
- }
127
- notify(ctx, 'Ponytail: ensuring plugin directory exists…', 'info');
128
- try {
129
- await fs.mkdir(pluginsDir, { recursive: true });
130
- }
131
- catch (e) {
132
- const m = `Ponytail update failed: failed to create ${pluginsDir}: ${e.message}`;
133
- return report(ctx, m, 'warning');
134
- }
135
- if (!pi.exec) {
136
- const m = 'Ponytail update failed: extension host does not provide exec';
137
- return report(ctx, m, 'warning');
138
- }
139
- notify(ctx, 'Ponytail: running npm install…', 'info');
140
- let out = '';
121
+ // Bundled with tersio: no separate package to refresh. `tersio update`
122
+ // pulls the bundled copy with the CLI.
123
+ let localVer = null;
141
124
  try {
142
- const r = await pi.exec('npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir });
143
- out = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
144
- if (r.code !== 0)
145
- throw new Error(r.stderr || `npm exited ${r.code}`);
125
+ const raw = await readTextIfExists(PONYTAIL_LOCAL);
126
+ if (raw)
127
+ localVer = JSON.parse(raw).version ?? null;
146
128
  }
147
- catch (e) {
148
- const m = `Ponytail update failed: ${e.message}`;
149
- return report(ctx, m, 'warning');
129
+ catch {
130
+ localVer = null;
150
131
  }
151
- const m = `Ponytail update finished.${out ? `\n${out}` : ''}\n${RELOAD_MSG}`;
152
- notify(ctx, 'Ponytail update finished. ' + RELOAD_MSG, 'info');
153
- return m;
132
+ void pi;
133
+ const m = dryRun
134
+ ? `Ponytail dry-run: bundled with tersio (local=${localVer || '—'}); run \`tersio update\` to refresh it.`
135
+ : `Ponytail is bundled with tersio (local=${localVer || '—'}); run \`tersio update\` to refresh it.\n${RELOAD_MSG}`;
136
+ return report(ctx, m, 'info');
154
137
  }
155
138
  async function updateRtk(ctx, dryRun = false) {
156
139
  let release;
@@ -29,7 +29,6 @@ const IS_WINDOWS = process.platform === 'win32';
29
29
  const HOME = os.homedir();
30
30
 
31
31
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
32
- const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
33
32
  const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
34
33
  const RTK_BINARY = path.join(HOME, '.bun', 'bin', IS_WINDOWS ? 'rtk.exe' : 'rtk');
35
34
  const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'agent', 'extensions', 'caveman-session', 'rule.md');
@@ -165,35 +164,18 @@ async function checkAddons(ctx: AddonUpdaterCtx): Promise<string> {
165
164
  }
166
165
 
167
166
  async function updatePonytail(pi: AddonUpdaterPi, ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
168
- const pluginsDir = path.join(HOME, '.omp', 'plugins');
169
- if (dryRun) {
170
- const m = `Ponytail dry-run: would run \`npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund\` in ${pluginsDir}.`;
171
- return report(ctx, m, 'info');
172
- }
173
- notify(ctx, 'Ponytail: ensuring plugin directory exists…', 'info');
174
- try {
175
- await fs.mkdir(pluginsDir, { recursive: true });
176
- } catch (e) {
177
- const m = `Ponytail update failed: failed to create ${pluginsDir}: ${(e as Error).message}`;
178
- return report(ctx, m, 'warning');
179
- }
180
- if (!pi.exec) {
181
- const m = 'Ponytail update failed: extension host does not provide exec';
182
- return report(ctx, m, 'warning');
183
- }
184
- notify(ctx, 'Ponytail: running npm install…', 'info');
185
- let out = '';
167
+ // Bundled with tersio: no separate package to refresh. `tersio update`
168
+ // pulls the bundled copy with the CLI.
169
+ let localVer: string | null = null;
186
170
  try {
187
- const r = await pi.exec('npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir });
188
- out = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
189
- if (r.code !== 0) throw new Error(r.stderr || `npm exited ${r.code}`);
190
- } catch (e) {
191
- const m = `Ponytail update failed: ${(e as Error).message}`;
192
- return report(ctx, m, 'warning');
193
- }
194
- const m = `Ponytail update finished.${out ? `\n${out}` : ''}\n${RELOAD_MSG}`;
195
- notify(ctx, 'Ponytail update finished. ' + RELOAD_MSG, 'info');
196
- return m;
171
+ const raw = await readTextIfExists(PONYTAIL_LOCAL);
172
+ if (raw) localVer = (JSON.parse(raw) as { version?: string }).version ?? null;
173
+ } catch { localVer = null; }
174
+ void pi;
175
+ const m = dryRun
176
+ ? `Ponytail dry-run: bundled with tersio (local=${localVer || '—'}); run \`tersio update\` to refresh it.`
177
+ : `Ponytail is bundled with tersio (local=${localVer || '—'}); run \`tersio update\` to refresh it.\n${RELOAD_MSG}`;
178
+ return report(ctx, m, 'info');
197
179
  }
198
180
 
199
181
  async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
@@ -1,9 +1,10 @@
1
1
  // Shared helpers for the installer and the AI add-ons updater.
2
2
  // Node built-ins only — this module must stay dependency-free.
3
3
  import { createHash } from 'node:crypto';
4
- import { createWriteStream } from 'node:fs';
4
+ import { createWriteStream, existsSync, mkdirSync, renameSync } from 'node:fs';
5
5
  import fs from 'node:fs/promises';
6
6
  import https from 'node:https';
7
+ import os from 'node:os';
7
8
  import path from 'node:path';
8
9
  // Promise.withResolvers is Node 22+; the package supports Node 18+.
9
10
  // ponytail: swap for the built-in when engines bumps to >=22.
@@ -149,6 +150,30 @@ export async function readTextIfExists(p) {
149
150
  return null;
150
151
  }
151
152
  }
153
+ // Tersio-owned data home: ~/.tersio (Windows: %USERPROFILE%\.tersio).
154
+ // First use moves legacy ~/.omp/plugins/tersio-* files over, so existing
155
+ // installs keep their history.
156
+ const migratedTersioFiles = new Set();
157
+ export function tersioHome() {
158
+ return path.join(os.homedir(), '.tersio');
159
+ }
160
+ export function tersioDataPath(name, legacy) {
161
+ const dest = path.join(tersioHome(), name);
162
+ if (!migratedTersioFiles.has(name)) {
163
+ migratedTersioFiles.add(name);
164
+ try {
165
+ if (!existsSync(dest)) {
166
+ const src = path.join(os.homedir(), '.omp', 'plugins', legacy);
167
+ if (existsSync(src)) {
168
+ mkdirSync(tersioHome(), { recursive: true });
169
+ renameSync(src, dest);
170
+ }
171
+ }
172
+ }
173
+ catch { /* best-effort; callers tolerate a missing file */ }
174
+ }
175
+ return dest;
176
+ }
152
177
  export function normalizeRtkVersion(value) {
153
178
  return String(value || '').replace(/^rtk\s+/i, '').replace(/^v/i, '').trim();
154
179
  }
@@ -2,9 +2,10 @@
2
2
  // Node built-ins only — this module must stay dependency-free.
3
3
 
4
4
  import { createHash } from 'node:crypto';
5
- import { createWriteStream } from 'node:fs';
5
+ import { createWriteStream, existsSync, mkdirSync, renameSync } from 'node:fs';
6
6
  import fs from 'node:fs/promises';
7
7
  import https from 'node:https';
8
+ import os from 'node:os';
8
9
  import path from 'node:path';
9
10
 
10
11
  export interface HttpOptions {
@@ -157,6 +158,32 @@ export async function readTextIfExists(p: string): Promise<string | null> {
157
158
  try { return await fs.readFile(p, 'utf8'); } catch { return null; }
158
159
  }
159
160
 
161
+ // Tersio-owned data home: ~/.tersio (Windows: %USERPROFILE%\.tersio).
162
+ // First use moves legacy ~/.omp/plugins/tersio-* files over, so existing
163
+ // installs keep their history.
164
+ const migratedTersioFiles = new Set<string>();
165
+
166
+ export function tersioHome(): string {
167
+ return path.join(os.homedir(), '.tersio');
168
+ }
169
+
170
+ export function tersioDataPath(name: string, legacy: string): string {
171
+ const dest = path.join(tersioHome(), name);
172
+ if (!migratedTersioFiles.has(name)) {
173
+ migratedTersioFiles.add(name);
174
+ try {
175
+ if (!existsSync(dest)) {
176
+ const src = path.join(os.homedir(), '.omp', 'plugins', legacy);
177
+ if (existsSync(src)) {
178
+ mkdirSync(tersioHome(), { recursive: true });
179
+ renameSync(src, dest);
180
+ }
181
+ }
182
+ } catch { /* best-effort; callers tolerate a missing file */ }
183
+ }
184
+ return dest;
185
+ }
186
+
160
187
  export function normalizeRtkVersion(value: string | undefined): string {
161
188
  return String(value || '').replace(/^rtk\s+/i, '').replace(/^v/i, '').trim();
162
189
  }
@@ -5,8 +5,8 @@
5
5
  // per-model table — list prices rot, the feed does not. Unknown models report
6
6
  // the default with known:false so the dashboard labels them honestly.
7
7
  import fs from 'node:fs';
8
- import os from 'node:os';
9
8
  import path from 'node:path';
9
+ import { tersioDataPath } from '../lib/utils.js';
10
10
  // Sonnet-class default for unpriced models; always paired with known:false.
11
11
  export const DEFAULT_PRICE = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
12
12
  const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
@@ -18,7 +18,7 @@ export function pricesCachePath() {
18
18
  const override = process.env.TERSIO_PRICES_FILE;
19
19
  if (override)
20
20
  return override;
21
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-prices.json');
21
+ return tersioDataPath('prices.json', 'tersio-prices.json');
22
22
  }
23
23
  export function pricesUrl() {
24
24
  return process.env.TERSIO_PRICES_URL || LITELLM_URL;
@@ -7,6 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
+ import { tersioDataPath } from '../lib/utils.ts';
10
11
 
11
12
  export interface ModelPrice {
12
13
  input: number;
@@ -27,7 +28,7 @@ let refreshInflight: Promise<boolean> | null = null;
27
28
  export function pricesCachePath(): string {
28
29
  const override = process.env.TERSIO_PRICES_FILE;
29
30
  if (override) return override;
30
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-prices.json');
31
+ return tersioDataPath('prices.json', 'tersio-prices.json');
31
32
  }
32
33
 
33
34
  export function pricesUrl(): string {
@@ -1,15 +1,16 @@
1
1
  // Shared usage ledger: append-only JSON-lines file. One source of truth read
2
- // by `tersio usage`, `/tersio usage`, and the gain dashboard.
2
+ // by `tersio usage`, `/tersio usage`, and the Dashboard.
3
3
  // Best-effort by design: a ledger failure never breaks the caller, and
4
4
  // corrupt lines are skipped on read (same pattern as the config normalizer).
5
5
  import fs from 'node:fs';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
+ import { tersioDataPath } from '../lib/utils.js';
8
9
  export function ledgerPath() {
9
10
  const override = process.env.TERSIO_USAGE_FILE;
10
11
  if (override)
11
12
  return override;
12
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-usage.jsonl');
13
+ return tersioDataPath('usage.jsonl', 'tersio-usage.jsonl');
13
14
  }
14
15
  // Reset watermark: tersio-owned timestamp marking the last statistics reset.
15
16
  // Session transcripts and the RTK database are host/tool-owned and never
@@ -20,7 +21,7 @@ export function resetMarkerPath() {
20
21
  const override = process.env.TERSIO_RESET_FILE;
21
22
  if (override)
22
23
  return override;
23
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-reset.json');
24
+ return tersioDataPath('reset.json', 'tersio-reset.json');
24
25
  }
25
26
  export function readResetWatermark() {
26
27
  try {
@@ -1,10 +1,11 @@
1
1
  // Shared usage ledger: append-only JSON-lines file. One source of truth read
2
- // by `tersio usage`, `/tersio usage`, and the gain dashboard.
2
+ // by `tersio usage`, `/tersio usage`, and the Dashboard.
3
3
  // Best-effort by design: a ledger failure never breaks the caller, and
4
4
  // corrupt lines are skipped on read (same pattern as the config normalizer).
5
5
  import fs from 'node:fs';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
+ import { tersioDataPath } from '../lib/utils.ts';
8
9
 
9
10
  export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
10
11
 
@@ -17,7 +18,7 @@ export interface UsageRow {
17
18
  export function ledgerPath(): string {
18
19
  const override = process.env.TERSIO_USAGE_FILE;
19
20
  if (override) return override;
20
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-usage.jsonl');
21
+ return tersioDataPath('usage.jsonl', 'tersio-usage.jsonl');
21
22
  }
22
23
 
23
24
  // Reset watermark: tersio-owned timestamp marking the last statistics reset.
@@ -28,7 +29,7 @@ export function ledgerPath(): string {
28
29
  export function resetMarkerPath(): string {
29
30
  const override = process.env.TERSIO_RESET_FILE;
30
31
  if (override) return override;
31
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-reset.json');
32
+ return tersioDataPath('reset.json', 'tersio-reset.json');
32
33
  }
33
34
 
34
35
  export function readResetWatermark(): number {
@@ -8,14 +8,14 @@
8
8
  // back to the live path. Never touches RTK's history.db or host transcripts.
9
9
  import { execFileSync } from 'node:child_process';
10
10
  import fs from 'node:fs';
11
- import os from 'node:os';
12
11
  import path from 'node:path';
13
12
  import { RECENT_LIMIT, canonicalModelId, classifySessionLine, codexSessionsDir, costOf, durOf, ingestSessionRow, newSessionAccum, sessionsDir, walkJsonl, } from './usage-ledger.js';
13
+ import { tersioDataPath } from '../lib/utils.js';
14
14
  export function usageDbPath() {
15
15
  const override = process.env.TERSIO_USAGE_DB;
16
16
  if (override)
17
17
  return override;
18
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-usage.db');
18
+ return tersioDataPath('usage.db', 'tersio-usage.db');
19
19
  }
20
20
  function esc(v) {
21
21
  return `'${v.replace(/'/g, "''")}'`;
@@ -129,8 +129,9 @@ function insertSql(file, r) {
129
129
  `${nullNum(r.d)},${r.cr},${r.cw},${nullNum(r.usd)},${esc(r.st)},` +
130
130
  `${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))});`;
131
131
  }
132
- // Incremental sync: unchanged transcripts are skipped via mtime+size, changed
133
- // ones are deleted and re-inserted, removed ones are dropped. True on success
132
+ // Incremental sync: unchanged transcripts are skipped via mtime+size,
133
+ // changed ones are deleted and re-inserted. Rows for deleted transcripts
134
+ // are kept, so OMP session rotation never erases history. True on success
134
135
  // (including nothing-to-do), false when sqlite3 or disk is unavailable.
135
136
  export function syncUsageDb() {
136
137
  let db;
@@ -168,8 +169,7 @@ export function syncUsageDb() {
168
169
  if (!prev || prev.mtime !== entry.mtime || prev.size !== entry.size)
169
170
  changed.push(file);
170
171
  }
171
- const removed = Object.keys(known).filter((p) => current[p] === undefined);
172
- if (!changed.length && !removed.length)
172
+ if (!changed.length)
173
173
  return true;
174
174
  const chunks = [];
175
175
  let chunkBytes = 0;
@@ -193,7 +193,7 @@ export function syncUsageDb() {
193
193
  return true;
194
194
  return flush();
195
195
  };
196
- for (const file of [...changed, ...removed]) {
196
+ for (const file of changed) {
197
197
  if (!push(`DELETE FROM messages WHERE file=${esc(file)};DELETE FROM files WHERE path=${esc(file)};`))
198
198
  return false;
199
199
  }
@@ -22,12 +22,13 @@ import {
22
22
  sessionsDir,
23
23
  walkJsonl,
24
24
  } from './usage-ledger.ts';
25
+ import { tersioDataPath } from '../lib/utils.ts';
25
26
  import type { RunStatus, SessionTokens } from './usage-ledger.ts';
26
27
 
27
28
  export function usageDbPath(): string {
28
29
  const override = process.env.TERSIO_USAGE_DB;
29
30
  if (override) return override;
30
- return path.join(os.homedir(), '.omp', 'plugins', 'tersio-usage.db');
31
+ return tersioDataPath('usage.db', 'tersio-usage.db');
31
32
  }
32
33
 
33
34
  export interface StoredUsage {
@@ -172,8 +173,9 @@ function insertSql(file: string, r: StoredRow): string {
172
173
  `${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))});`;
173
174
  }
174
175
 
175
- // Incremental sync: unchanged transcripts are skipped via mtime+size, changed
176
- // ones are deleted and re-inserted, removed ones are dropped. True on success
176
+ // Incremental sync: unchanged transcripts are skipped via mtime+size,
177
+ // changed ones are deleted and re-inserted. Rows for deleted transcripts
178
+ // are kept, so OMP session rotation never erases history. True on success
177
179
  // (including nothing-to-do), false when sqlite3 or disk is unavailable.
178
180
  export function syncUsageDb(): boolean {
179
181
  let db: string;
@@ -207,8 +209,7 @@ export function syncUsageDb(): boolean {
207
209
  const prev = known[file];
208
210
  if (!prev || prev.mtime !== entry.mtime || prev.size !== entry.size) changed.push(file);
209
211
  }
210
- const removed = Object.keys(known).filter((p) => current[p] === undefined);
211
- if (!changed.length && !removed.length) return true;
212
+ if (!changed.length) return true;
212
213
  const chunks: string[] = [];
213
214
  let chunkBytes = 0;
214
215
  const flush = (): boolean => {
@@ -228,7 +229,7 @@ export function syncUsageDb(): boolean {
228
229
  if (chunkBytes < 400_000) return true;
229
230
  return flush();
230
231
  };
231
- for (const file of [...changed, ...removed]) {
232
+ for (const file of changed) {
232
233
  if (!push(`DELETE FROM messages WHERE file=${esc(file)};DELETE FROM files WHERE path=${esc(file)};`)) return false;
233
234
  }
234
235
  try {
@@ -1,4 +1,4 @@
1
- // /tersio root command — status, check, update, gain, usage, help.
1
+ // /tersio root command — status, check, update, dashboard, usage, help.
2
2
  // Mode switches live on their own commands (/caveman, /rtk, /combo, and the
3
3
  // upstream /ponytail): the router keeps no redundant copies. Shared-state
4
4
  // publishes sync sibling mirrors live, so switches notify and take effect
@@ -12,7 +12,7 @@ const HELP = [
12
12
  '/tersio status — active modes + combo level',
13
13
  '/tersio check — add-on version check',
14
14
  '/tersio update <ponytail|rtk|caveman|all> [--dry-run]',
15
- '/tersio gain — savings summary + dashboard hint',
15
+ '/tersio dashboard — open the Dashboard',
16
16
  '/tersio usage — ledger report for this machine',
17
17
  '/tersio help — this table',
18
18
  'mode switches: /caveman /rtk /combo (tersio) + /ponytail (upstream)',
@@ -33,27 +33,24 @@ function usageSummary() {
33
33
  const last = new Date(rows[rows.length - 1].ts).toISOString();
34
34
  return `tersio usage: ${rows.length} rows (${parts.join(', ')}), last write ${last}.`;
35
35
  }
36
- // Gain opens instantaneously in the default browser: export a file://-ready
37
- // snapshot (data inlined, no server to babysit) and open it. Falls back to
38
- // the shell command when export or open fails.
39
- async function openGainDashboard(pi, ctx) {
40
- const file = path.join(os.tmpdir(), `tersio-gain-${Date.now()}.html`);
36
+ async function openDashboard(pi, ctx) {
37
+ const file = path.join(os.tmpdir(), `tersio-dashboard-${Date.now()}.html`);
41
38
  try {
42
- const exported = await pi.exec?.('tersio', ['gain', '--export', file], { cwd: ctx?.cwd });
39
+ const exported = await pi.exec?.('tersio', ['dashboard', '--export', file], { cwd: ctx?.cwd });
43
40
  if (!exported || exported.code !== 0)
44
41
  throw new Error((exported?.stderr || 'export failed').trim());
45
42
  const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
46
43
  await pi.exec?.(openCmd, [file], { cwd: ctx?.cwd });
47
- ctx?.ui?.notify?.(`tersio gain: dashboard opened in your browser.`, 'info');
44
+ ctx?.ui?.notify?.(`tersio dashboard: opened in your browser.`, 'info');
48
45
  }
49
46
  catch (e) {
50
- ctx?.ui?.notify?.(`tersio gain: could not open dashboard (${e.message}). Run 'tersio gain --open' in a shell.`, 'warning');
47
+ ctx?.ui?.notify?.(`tersio dashboard: could not open (${e.message}). Run 'tersio dashboard --open' in a shell.`, 'warning');
51
48
  }
52
49
  }
53
50
  export default function tersioCommandsExtension(pi) {
54
51
  pi.setLabel?.('Tersio unified root command');
55
52
  pi.registerCommand?.('tersio', {
56
- description: 'Tersio root: status|check|update|gain|usage|help (modes: /caveman /rtk /combo /ponytail)',
53
+ description: 'Tersio root: status|check|update|dashboard|usage|help',
57
54
  handler: async (args, ctx) => {
58
55
  const parts = String(args || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
59
56
  const [sub] = [parts[0]];
@@ -82,8 +79,8 @@ export default function tersioCommandsExtension(pi) {
82
79
  await runAddonUpdate(pi, ctx, target, dryRun);
83
80
  return;
84
81
  }
85
- if (sub === 'gain') {
86
- await openGainDashboard(pi, ctx);
82
+ if (sub === 'dashboard') {
83
+ await openDashboard(pi, ctx);
87
84
  return;
88
85
  }
89
86
  if (sub === 'usage') {
@@ -1,4 +1,4 @@
1
- // /tersio root command — status, check, update, gain, usage, help.
1
+ // /tersio root command — status, check, update, dashboard, usage, help.
2
2
  // Mode switches live on their own commands (/caveman, /rtk, /combo, and the
3
3
  // upstream /ponytail): the router keeps no redundant copies. Shared-state
4
4
  // publishes sync sibling mirrors live, so switches notify and take effect
@@ -18,7 +18,7 @@ const HELP = [
18
18
  '/tersio status — active modes + combo level',
19
19
  '/tersio check — add-on version check',
20
20
  '/tersio update <ponytail|rtk|caveman|all> [--dry-run]',
21
- '/tersio gain — savings summary + dashboard hint',
21
+ '/tersio dashboard — open the Dashboard',
22
22
  '/tersio usage — ledger report for this machine',
23
23
  '/tersio help — this table',
24
24
  'mode switches: /caveman /rtk /combo (tersio) + /ponytail (upstream)',
@@ -40,19 +40,16 @@ function usageSummary(): string {
40
40
  return `tersio usage: ${rows.length} rows (${parts.join(', ')}), last write ${last}.`;
41
41
  }
42
42
 
43
- // Gain opens instantaneously in the default browser: export a file://-ready
44
- // snapshot (data inlined, no server to babysit) and open it. Falls back to
45
- // the shell command when export or open fails.
46
- async function openGainDashboard(pi: ExtensionApi, ctx?: ExtensionCtx): Promise<void> {
47
- const file = path.join(os.tmpdir(), `tersio-gain-${Date.now()}.html`);
43
+ async function openDashboard(pi: ExtensionApi, ctx?: ExtensionCtx): Promise<void> {
44
+ const file = path.join(os.tmpdir(), `tersio-dashboard-${Date.now()}.html`);
48
45
  try {
49
- const exported = await pi.exec?.('tersio', ['gain', '--export', file], { cwd: ctx?.cwd });
46
+ const exported = await pi.exec?.('tersio', ['dashboard', '--export', file], { cwd: ctx?.cwd });
50
47
  if (!exported || exported.code !== 0) throw new Error((exported?.stderr || 'export failed').trim());
51
48
  const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
52
49
  await pi.exec?.(openCmd, [file], { cwd: ctx?.cwd });
53
- ctx?.ui?.notify?.(`tersio gain: dashboard opened in your browser.`, 'info');
50
+ ctx?.ui?.notify?.(`tersio dashboard: opened in your browser.`, 'info');
54
51
  } catch (e) {
55
- ctx?.ui?.notify?.(`tersio gain: could not open dashboard (${(e as Error).message}). Run 'tersio gain --open' in a shell.`, 'warning');
52
+ ctx?.ui?.notify?.(`tersio dashboard: could not open (${(e as Error).message}). Run 'tersio dashboard --open' in a shell.`, 'warning');
56
53
  }
57
54
  }
58
55
 
@@ -61,7 +58,7 @@ export default function tersioCommandsExtension(pi: ExtensionApi): void {
61
58
 
62
59
 
63
60
  pi.registerCommand?.('tersio', {
64
- description: 'Tersio root: status|check|update|gain|usage|help (modes: /caveman /rtk /combo /ponytail)',
61
+ description: 'Tersio root: status|check|update|dashboard|usage|help',
65
62
  handler: async (args, ctx) => {
66
63
  const parts = String(args || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
67
64
  const [sub] = [parts[0]];
@@ -90,8 +87,8 @@ export default function tersioCommandsExtension(pi: ExtensionApi): void {
90
87
  await runAddonUpdate(pi, ctx, target, dryRun);
91
88
  return;
92
89
  }
93
- if (sub === 'gain') {
94
- await openGainDashboard(pi, ctx);
90
+ if (sub === 'dashboard') {
91
+ await openDashboard(pi, ctx);
95
92
  return;
96
93
  }
97
94
  if (sub === 'usage') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.21.0",
3
+ "version": "2.22.1",
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",
@@ -25,43 +25,23 @@
25
25
  "omp": {
26
26
  "name": "Tersio",
27
27
  "description": "Toggleable Caveman/RTK/Ponytail session modes with Combo presets",
28
- "extensions": [],
28
+ "extensions": [
29
+ "./extensions/caveman-session/index.ts",
30
+ "./extensions/rtk-session/index.ts",
31
+ "./extensions/combo-toggle/index.ts",
32
+ "./extensions/shared/mode-reinforcement.ts",
33
+ "./extensions/tersio-commands/index.ts"
34
+ ],
35
+ "skills": [
36
+ "./node_modules/@dietrichgebert/ponytail/skills"
37
+ ],
29
38
  "features": {
30
- "caveman": {
31
- "description": "Caveman terse-reply session mode (/caveman)",
32
- "default": true,
33
- "extensions": [
34
- "./extensions/caveman-session/index.ts"
35
- ]
36
- },
37
- "rtk": {
38
- "description": "RTK compact shell-output session mode (/rtk)",
39
- "default": true,
40
- "extensions": [
41
- "./extensions/rtk-session/index.ts"
42
- ]
43
- },
44
- "ponytail": {
45
- "description": "Combo presets + Ponytail mode reinforcement (/combo)",
46
- "default": true,
47
- "extensions": [
48
- "./extensions/combo-toggle/index.ts",
49
- "./extensions/shared/mode-reinforcement.ts"
50
- ]
51
- },
52
39
  "updater": {
53
40
  "description": "In-session updater for Ponytail, RTK, and Caveman (/ai-addons)",
54
41
  "default": true,
55
42
  "extensions": [
56
43
  "./extensions/ai-addons-updater/index.ts"
57
44
  ]
58
- },
59
- "tersio": {
60
- "description": "Unified /tersio root command",
61
- "default": true,
62
- "extensions": [
63
- "./extensions/tersio-commands/index.ts"
64
- ]
65
45
  }
66
46
  },
67
47
  "settings": {
@@ -107,7 +87,7 @@
107
87
  },
108
88
  "currency": {
109
89
  "type": "enum",
110
- "description": "Display currency for usage/gain reports (--currency overrides per run)",
90
+ "description": "Display currency for usage/dashboard reports (--currency overrides per run)",
111
91
  "values": [
112
92
  "USD",
113
93
  "PHP",
@@ -127,17 +107,21 @@
127
107
  "files": [
128
108
  "tersio.js",
129
109
  "cli/**/*.js",
130
- "dashboard/**/*",
110
+ "dashboard/dist/**/*",
131
111
  "extensions/**/*.js",
132
112
  "extensions/**/*.ts"
133
113
  ],
134
114
  "scripts": {
135
- "clean": "rm -f tersio.js cli/*.js extensions/**/*.js",
136
- "build": "tsc",
115
+ "clean": "rm -f tersio.js cli/*.js extensions/**/*.js && rm -rf dashboard/dist",
116
+ "build": "tsc && bun run build:dashboard",
117
+ "build:dashboard": "bun install --cwd dashboard/app --frozen-lockfile && bun run --cwd dashboard/app build",
137
118
  "prepublishOnly": "bun run build",
138
119
  "check": "tsc --noEmit",
139
120
  "doctor": "node tersio.js --doctor",
140
121
  "dry-run": "node tersio.js --dry-run --scope user --yes",
122
+ "lint": "oxlint cli extensions test tersio.ts",
123
+ "lint:dashboard": "bun run --cwd dashboard/app lint",
124
+ "lint:fix": "oxlint cli extensions test tersio.ts --fix",
141
125
  "test": "vitest run"
142
126
  },
143
127
  "engines": {
@@ -145,10 +129,12 @@
145
129
  },
146
130
  "devDependencies": {
147
131
  "@types/node": "^26.4.1",
132
+ "oxlint": "^1.85.0",
148
133
  "typescript": "^7.0.2",
149
134
  "vitest": "^5.0.1"
150
135
  },
151
136
  "dependencies": {
152
- "@clack/prompts": "^1.8.0"
137
+ "@clack/prompts": "^1.8.0",
138
+ "@dietrichgebert/ponytail": "^4.10.0"
153
139
  }
154
140
  }