@krtclcdy/tersio 2.9.0 → 2.10.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.
@@ -0,0 +1,61 @@
1
+ // extensions/shared/rtk-gain.ts — measured RTK savings from its own store.
2
+ // RTK records every invocation (input/output/saved tokens, savings %, exec
3
+ // time) in history.db; `rtk gain` renders the same rows. We read them with
4
+ // the sqlite3 CLI (present on macOS; best-effort elsewhere) so the dashboard
5
+ // shows measured figures, never estimates. Missing DB/CLI → empty, no throw.
6
+ //
7
+ // Caveman and Ponytail have no per-command counters (instruction-following
8
+ // isn't metered); their gains are bench-measured in BENCHMARK.md and cited
9
+ // as static figures wherever RTK rows appear.
10
+ import { execFileSync } from 'node:child_process';
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ const EMPTY = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0, byCommand: [] };
15
+ export function rtkDbPath() {
16
+ const override = process.env.TERSIO_RTK_DB;
17
+ if (override)
18
+ return override;
19
+ if (process.platform === 'darwin') {
20
+ return path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'history.db');
21
+ }
22
+ const xdg = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
23
+ return path.join(xdg, 'rtk', 'history.db');
24
+ }
25
+ function query(db, sql) {
26
+ const out = execFileSync('sqlite3', ['-separator', '\t', db, sql], {
27
+ encoding: 'utf8',
28
+ timeout: 5000,
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ });
31
+ return out
32
+ .split('\n')
33
+ .filter((line) => line.trim())
34
+ .map((line) => line.split('\t'));
35
+ }
36
+ export function readRtkGain(limit = 10) {
37
+ try {
38
+ if (!fs.existsSync(rtkDbPath()))
39
+ return EMPTY;
40
+ const [[commands = '0', saved = '0', input = '0', avgPct = '0', totalMs = '0'] = []] = query(rtkDbPath(), 'SELECT COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(SUM(input_tokens),0), COALESCE(AVG(savings_pct),0), COALESCE(SUM(exec_time_ms),0) FROM commands;');
41
+ const byCommand = query(rtkDbPath(), `SELECT rtk_cmd, COUNT(*), SUM(saved_tokens), AVG(savings_pct), AVG(exec_time_ms) FROM commands GROUP BY rtk_cmd ORDER BY SUM(saved_tokens) DESC LIMIT ${Math.max(1, Math.floor(limit))};`).map(([command, count, savedRow, pct, ms]) => ({
42
+ command,
43
+ count: Number(count) || 0,
44
+ saved: Number(savedRow) || 0,
45
+ avgPct: Number(pct) || 0,
46
+ avgMs: Number(ms) || 0,
47
+ }));
48
+ const totalIn = Number(input) || 0;
49
+ return {
50
+ commands: Number(commands) || 0,
51
+ saved: Number(saved) || 0,
52
+ input: totalIn,
53
+ avgPct: totalIn ? ((Number(saved) || 0) / totalIn) * 100 : 0,
54
+ totalMs: Number(totalMs) || 0,
55
+ byCommand,
56
+ };
57
+ }
58
+ catch {
59
+ return EMPTY;
60
+ }
61
+ }
@@ -35,6 +35,15 @@ export function sessionsDir() {
35
35
  return override;
36
36
  return path.join(os.homedir(), '.omp', 'agent', 'sessions');
37
37
  }
38
+ export function codexSessionsDir() {
39
+ const override = process.env.TERSIO_CODEX_DIR;
40
+ if (override)
41
+ return override;
42
+ const codexHome = process.env.CODEX_HOME;
43
+ if (codexHome)
44
+ return path.join(codexHome, 'sessions');
45
+ return path.join(os.homedir(), '.codex', 'sessions');
46
+ }
38
47
  function dayKey(ts) {
39
48
  const ms = typeof ts === 'number' ? ts : Date.parse(ts);
40
49
  if (!Number.isFinite(ms))
@@ -66,11 +75,35 @@ export function importSessionTokens() {
66
75
  const byDay = {};
67
76
  const byDayModel = {};
68
77
  const byTool = {};
78
+ const byModelMessages = {};
69
79
  const totals = zeroBreakdown();
70
80
  let messages = 0;
71
81
  let costMeasured = 0;
72
82
  const files = [];
73
83
  walkJsonl(sessionsDir(), files, 2000);
84
+ // A sessions override signals an isolated environment (tests, fixtures):
85
+ // only walk the real codex dir when it is explicitly set.
86
+ if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
87
+ walkJsonl(codexSessionsDir(), files, 2000);
88
+ }
89
+ let codexProvider = null;
90
+ function ingest(model, usage, ts) {
91
+ addInto(totals, usage);
92
+ byModel[model] ??= zeroBreakdown();
93
+ addInto(byModel[model], usage);
94
+ byModelMessages[model] = (byModelMessages[model] ?? 0) + 1;
95
+ const day = ts !== undefined ? dayKey(ts) : null;
96
+ if (day) {
97
+ byDay[day] ??= zeroBreakdown();
98
+ addInto(byDay[day], usage);
99
+ const sum = ['input', 'output', 'cacheRead', 'cacheWrite'].reduce((a, k) => a + (typeof usage[k] === 'number' && Number.isFinite(usage[k]) ? Math.floor(usage[k]) : 0), 0);
100
+ if (sum > 0) {
101
+ byDayModel[day] ??= {};
102
+ byDayModel[day][model] = (byDayModel[day][model] ?? 0) + sum;
103
+ }
104
+ }
105
+ messages += 1;
106
+ }
74
107
  for (const file of files) {
75
108
  let text;
76
109
  try {
@@ -84,24 +117,30 @@ export function importSessionTokens() {
84
117
  continue;
85
118
  try {
86
119
  const row = JSON.parse(line);
120
+ const codexRow = row;
121
+ const payload = codexRow.payload;
122
+ if (payload && typeof payload === 'object') {
123
+ if (codexRow.type === 'session_meta' && typeof payload.model_provider === 'string') {
124
+ codexProvider = payload.model_provider;
125
+ }
126
+ else if (payload.type === 'token_count') {
127
+ const last = payload.info?.last_token_usage;
128
+ if (last) {
129
+ ingest(codexProvider ? `codex/${codexProvider}` : 'codex', {
130
+ input: last['input_tokens'],
131
+ output: last['output_tokens'],
132
+ cacheRead: last['cached_input_tokens'],
133
+ cacheWrite: last['cache_write_input_tokens'],
134
+ }, row.timestamp);
135
+ continue;
136
+ }
137
+ }
138
+ }
87
139
  const msg = row.message;
88
140
  if (!msg || msg.role !== 'assistant' || !msg.usage)
89
141
  continue;
90
142
  const model = typeof msg.model === 'string' && msg.model ? msg.model : 'unknown';
91
- addInto(totals, msg.usage);
92
- byModel[model] ??= zeroBreakdown();
93
- addInto(byModel[model], msg.usage);
94
- const day = row.timestamp !== undefined ? dayKey(row.timestamp) : null;
95
- if (day) {
96
- byDay[day] ??= zeroBreakdown();
97
- addInto(byDay[day], msg.usage);
98
- const n = msg.usage;
99
- const sum = ['input', 'output', 'cacheRead', 'cacheWrite'].reduce((a, k) => a + (typeof n[k] === 'number' && Number.isFinite(n[k]) ? Math.floor(n[k]) : 0), 0);
100
- if (sum > 0) {
101
- byDayModel[day] ??= {};
102
- byDayModel[day][model] = (byDayModel[day][model] ?? 0) + sum;
103
- }
104
- }
143
+ ingest(model, msg.usage, row.timestamp);
105
144
  if (typeof msg.usage.cost === 'number' && Number.isFinite(msg.usage.cost))
106
145
  costMeasured += msg.usage.cost;
107
146
  for (const part of msg.content ?? []) {
@@ -110,12 +149,11 @@ export function importSessionTokens() {
110
149
  byTool[key] = (byTool[key] ?? 0) + 1;
111
150
  }
112
151
  }
113
- messages += 1;
114
152
  }
115
153
  catch { /* skip corrupt lines */ }
116
154
  }
117
155
  }
118
- return { messages, totals, byModel, byDay, byDayModel, byTool, costMeasured };
156
+ return { messages, totals, byModel, byDay, byDayModel, byTool, byModelMessages, costMeasured };
119
157
  }
120
158
  // Lead binary of a shell string: first segment head past `cd` chains and
121
159
  // VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
@@ -134,24 +172,11 @@ function leadBinary(command) {
134
172
  }
135
173
  return 'bash';
136
174
  }
137
- const PRICE_TABLE = [
138
- { match: ':free', price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
139
- { match: 'opus', price: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 } },
140
- { match: 'sonnet', price: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } },
141
- { match: 'haiku', price: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 } },
142
- { match: 'gpt', price: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 2.5 } },
143
- { match: 'gemini', price: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 1.25 } },
144
- { match: 'glm', price: { input: 0.5, output: 1, cacheRead: 0.05, cacheWrite: 0.5 } },
145
- ];
146
- const DEFAULT_PRICE = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
147
- export function priceFor(model) {
148
- const name = model.toLowerCase();
149
- for (const row of PRICE_TABLE) {
150
- if (name.includes(row.match))
151
- return { price: row.price, known: true };
152
- }
153
- return { price: DEFAULT_PRICE, known: false };
154
- }
175
+ // --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
176
+ import { DEFAULT_PRICE, priceFor } from './pricing.js';
177
+ import { co2GramsFor, energyWhFor } from './carbon.js';
178
+ export { DEFAULT_PRICE, priceFor };
179
+ export { co2GramsFor, energyWhFor };
155
180
  export function usdCost(t, model) {
156
181
  const { price, known } = model ? priceFor(model) : { price: DEFAULT_PRICE, known: false };
157
182
  const m = 1 / 1_000_000;
@@ -160,11 +185,13 @@ export function usdCost(t, model) {
160
185
  priced: model ? known : false,
161
186
  };
162
187
  }
163
- // ponytail: CO2 is a rough server-energy multiple on output tokens, not metered.
188
+ // CO2 now model-differentiated via ./carbon.ts (EcoLogits 0.8.2 port).
164
189
  // Always render with ~est. and never merge with measured figures.
190
+ // The constant below is the served gCO2eq per 1K output tokens for the
191
+ // default (gpt-4o-class) model, kept so single-figure callers stay honest.
165
192
  export const CO2_G_PER_1K_OUTPUT = 0.2;
166
- export function co2Grams(outputTokens) {
167
- return (outputTokens / 1000) * CO2_G_PER_1K_OUTPUT;
193
+ export function co2Grams(outputTokens, model) {
194
+ return co2GramsFor(model ?? 'gpt-4o', outputTokens);
168
195
  }
169
196
  export function readUsage() {
170
197
  let text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.9.0",
3
+ "version": "2.10.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",
@@ -115,8 +115,7 @@
115
115
  "files": [
116
116
  "tersio.js",
117
117
  "cli/**/*.js",
118
- "cli/dashboard.html",
119
- "cli/brand.webp",
118
+ "dashboard/**/*",
120
119
  "extensions/**/*.js"
121
120
  ],
122
121
  "scripts": {