@krtclcdy/tersio 2.22.1 → 2.24.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.
Files changed (59) hide show
  1. package/README.md +62 -46
  2. package/cli/agent-hosts.js +42 -0
  3. package/cli/agents.js +266 -0
  4. package/cli/common.js +55 -55
  5. package/cli/currency.js +8 -12
  6. package/cli/dashboard.js +76 -49
  7. package/cli/doctor-fix.js +61 -44
  8. package/cli/doctor.js +66 -44
  9. package/cli/hosts.js +118 -0
  10. package/cli/install.js +187 -143
  11. package/cli/interactive.js +27 -1
  12. package/cli/layer.js +24 -0
  13. package/cli/manifest.js +36 -0
  14. package/cli/omp-layer.js +51 -0
  15. package/cli/pi-layer.js +37 -0
  16. package/cli/pi-wiring.js +92 -0
  17. package/cli/profile.js +45 -46
  18. package/cli/reset.js +3 -26
  19. package/cli/rtk-wiring.js +10 -15
  20. package/cli/settings.js +1 -1
  21. package/cli/uninstall.js +135 -44
  22. package/cli/update.js +24 -30
  23. package/cli/usage.js +6 -1
  24. package/dashboard/dist/index.html +16 -16
  25. package/extensions/ai-addons-updater/index.js +36 -25
  26. package/extensions/ai-addons-updater/index.ts +36 -24
  27. package/extensions/caveman-session/index.js +23 -18
  28. package/extensions/caveman-session/index.ts +23 -18
  29. package/extensions/caveman-session/rule.md +90 -0
  30. package/extensions/combo-toggle/index.js +63 -41
  31. package/extensions/combo-toggle/index.ts +61 -48
  32. package/extensions/lib/utils.js +56 -13
  33. package/extensions/lib/utils.ts +55 -13
  34. package/extensions/rtk-session/index.js +20 -19
  35. package/extensions/rtk-session/index.ts +18 -24
  36. package/extensions/shared/carbon.js +5 -8
  37. package/extensions/shared/carbon.ts +5 -8
  38. package/extensions/shared/host.js +68 -0
  39. package/extensions/shared/host.ts +104 -0
  40. package/extensions/shared/omp-prompt.js +12 -0
  41. package/extensions/shared/plugin-settings.js +63 -16
  42. package/extensions/shared/plugin-settings.ts +67 -17
  43. package/extensions/shared/pricing.js +26 -15
  44. package/extensions/shared/pricing.ts +28 -16
  45. package/extensions/shared/rtk-gain.js +13 -24
  46. package/extensions/shared/rtk-gain.ts +14 -25
  47. package/extensions/shared/session-state.js +26 -28
  48. package/extensions/shared/session-state.ts +26 -29
  49. package/extensions/shared/types.ts +15 -3
  50. package/extensions/shared/usage-ledger.js +136 -38
  51. package/extensions/shared/usage-ledger.ts +149 -52
  52. package/extensions/shared/usage-store.js +14 -12
  53. package/extensions/shared/usage-store.ts +16 -14
  54. package/extensions/tersio-commands/index.js +2 -1
  55. package/extensions/tersio-commands/index.ts +2 -1
  56. package/package.json +15 -7
  57. package/tersio.js +6 -6
  58. package/extensions/shared/mode-reinforcement.js +0 -26
  59. package/extensions/shared/mode-reinforcement.ts +0 -29
@@ -12,9 +12,9 @@ export const COMBO_LEVELS: Record<string, Readonly<ComboState>> = Object.freeze(
12
12
  });
13
13
 
14
14
  const MODE_VALUES: Record<string, Set<string>> = {
15
- caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan']),
15
+ caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
16
16
  rtk: new Set(['off', 'on']),
17
- ponytail: new Set(['off', 'lite', 'full', 'ultra', 'review']),
17
+ ponytail: new Set(['off', 'lite', 'full', 'ultra']),
18
18
  };
19
19
 
20
20
  type ModeName = 'caveman' | 'rtk' | 'ponytail';
@@ -34,7 +34,8 @@ interface Bridge {
34
34
  export function normalizeMode(name: ModeName, value: unknown): string | null {
35
35
  if (name === 'rtk' && typeof value === 'boolean') return value ? 'on' : 'off';
36
36
  const mode = String(value ?? '').trim().toLowerCase();
37
- return MODE_VALUES[name]?.has(mode) ? mode : null;
37
+ if (!MODE_VALUES[name]?.has(mode)) return null;
38
+ return name === 'caveman' && mode === 'wenyan' ? 'wenyan-full' : mode;
38
39
  }
39
40
 
40
41
  export function deriveLevel(modes: Modes | null | undefined): ComboLevel {
@@ -97,24 +98,23 @@ export function normalizeComboLevel(value: unknown): ComboLevel | null {
97
98
  return isPresetLevel(level) ? level : null;
98
99
  }
99
100
 
100
- export function paintStatusBar(ui: UiApi | undefined, key: string, emoji: string, label: string, isActive: boolean): void {
101
+ // OMP themes status text through ctx.ui.theme.fg; pi has no ctx.ui.theme, so
102
+ // the same string renders plain there.
103
+ export function themeStatus(ui: UiApi | undefined, indicator: string, label: string, highlight = false): string {
101
104
  const theme = ui?.theme;
102
- const indicator = isActive && theme?.fg ? theme.fg('accent', emoji) : emoji;
103
- ui?.setStatus?.(key, theme?.fg ? `${indicator} ${theme.fg('muted', label)}` : `${indicator} ${label}`);
104
- }
105
-
106
- // Which context a status bar should remember to paint through.
107
- //
108
- // A UI-less event — a headless child session_start, or a top-level
109
- // session_start that fires before the TUI attaches — must never become the
110
- // remembered context. Two things went wrong when it did: the old
111
- // `ctx || lastCtx` fallback only triggered for a *falsy* ctx, so a truthy
112
- // ctx without `ui` returned early anyway, and remembering it destroyed the one
113
- // paintable context we had. Every later bare syncStatus() then went mute —
114
- // and the bare call is the bridge-listener path, which is how a mode change
115
- // made by a sibling extension reaches this bar. The symptom is a bar frozen on
116
- // a preset the session no longer has: it shows "combo BALANCED" while caveman
117
- // and rtk are already off.
105
+ if (!theme?.fg) return `${indicator} ${label}`;
106
+ return `${highlight ? theme.fg('accent', indicator) : indicator} ${theme.fg('muted', label)}`;
107
+ }
108
+
109
+ export function paintStatusBar(ui: UiApi | undefined, key: string, emoji: string, label: string, isActive: boolean): void {
110
+ ui?.setStatus?.(key, themeStatus(ui, emoji, label, isActive));
111
+ }
112
+
113
+ // Which context a status bar paints through. A UI-less event (a headless child
114
+ // session_start, or a top-level one that fires before the TUI attaches) must
115
+ // never be remembered: doing so mutes every later bare syncStatus(), which is
116
+ // the bridge-listener path a sibling's mode change arrives on. The symptom is a
117
+ // bar frozen on a preset the session no longer has.
118
118
  export function paintableCtx(remembered: ExtensionCtx | undefined, next: ExtensionCtx | undefined): ExtensionCtx | undefined {
119
119
  return next?.ui?.setStatus ? next : remembered;
120
120
  }
@@ -155,8 +155,7 @@ export function setSharedComboMode(name: ModeName, value: unknown): Readonly<Com
155
155
  if (!mode) return getSharedComboState();
156
156
  const modes = { ...getSharedComboState(), [name]: mode } as Modes;
157
157
  // Derive the level from the final triplet so a redundant same-value write
158
- // (entry replay on resume, upstream ponytail re-affirming its mode) cannot
159
- // strand a matching preset at 'custom' and drop the combo bar.
158
+ // cannot strand a matching preset at 'custom' and drop the combo bar.
160
159
  return publish(normalizedState(modes));
161
160
  }
162
161
 
@@ -177,21 +176,19 @@ export function reconcileSharedComboEntries(entries: SessionEntry[] | null | und
177
176
  if (mode) modes[name] = mode;
178
177
  }
179
178
  }
180
- // The level always reflects the final mode triplet, never the write order:
181
- // a preset entry followed by the same values reconciles back to the preset.
179
+ // The level reflects the final triplet, never the write order, so a preset
180
+ // entry followed by the same values reconciles back to the preset.
182
181
  return publish(normalizedState(modes));
183
182
  }
184
183
 
185
- // One-line session-wide mode summary for change confirmations, e.g.
186
- // `caveman=ULTRA, rtk=ON, ponytail=ULTRA`.
184
+ // One-line session-wide mode summary for change confirmations.
187
185
  export function activeModesSummary(state: { caveman: string; rtk: string; ponytail: string }): string {
188
186
  return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
189
187
  }
190
188
 
191
189
  export function setSharedComboListener(listener: ((state: Readonly<ComboState>) => void) | null): void {
192
- // Additive: every sibling extension syncs its local mirror on publish, so
193
- // a /tersio or /combo switch takes effect next turn with no session reload.
194
- // A null listener clears all (no current callers; reserved for teardown).
190
+ // Additive: every sibling syncs its mirror on publish, so a /tersio or
191
+ // /combo switch lands next turn with no reload. A null listener clears all.
195
192
  if (typeof listener === 'function') bridge().listeners.add(listener);
196
193
  else bridge().listeners.clear();
197
194
  }
@@ -22,6 +22,11 @@ export type SessionEntry = {
22
22
  };
23
23
 
24
24
  export interface UiApi {
25
+ select?: (
26
+ title: string,
27
+ options: Array<string | { label: string; description?: string }>,
28
+ dialogOptions?: Record<string, unknown>,
29
+ ) => Promise<string | undefined>;
25
30
  setStatus?: (name: string, value: string | undefined) => void;
26
31
  notify?: (message: string, level?: string) => void;
27
32
  theme?: {
@@ -42,8 +47,16 @@ export interface ExtensionCtx {
42
47
 
43
48
  export interface SystemPromptEvent {
44
49
  systemPrompt: string | string[];
50
+ // pi only: the mutable prompt sections. OMP does not provide it.
51
+ systemPromptOptions?: { appendSystemPrompt?: string };
45
52
  }
46
53
 
54
+ /** What a `before_agent_start` handler hands back. OMP takes an array, pi a string. */
55
+ export type PromptInjection = { systemPrompt: string[] } | { systemPrompt: string };
56
+
57
+ /** A tool `parameters` object. OMP wants zod, pi wants JSON Schema. */
58
+ export type ToolParams = Record<string, unknown>;
59
+
47
60
  export interface InputEvent {
48
61
  text?: string;
49
62
  source?: string;
@@ -63,7 +76,7 @@ export interface ExtensionApi {
63
76
  execute: (
64
77
  toolCallId: string,
65
78
  params: { args: string[] },
66
- signal: AbortSignal,
79
+ signal: AbortSignal | undefined,
67
80
  onUpdate: ((data: unknown) => void) | undefined,
68
81
  ctx: ExtensionCtx | undefined,
69
82
  ) => Promise<{
@@ -81,5 +94,4 @@ export interface ExtensionApi {
81
94
  }>;
82
95
  cwd?: string;
83
96
  zod?: { z: unknown };
84
- on<E>(event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void;
85
- }
97
+ on<E>(event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void;}
@@ -1,22 +1,17 @@
1
- // Shared usage ledger: append-only JSON-lines file. One source of truth read
2
- // by `tersio usage`, `/tersio usage`, and the Dashboard.
3
- // Best-effort by design: a ledger failure never breaks the caller, and
4
- // corrupt lines are skipped on read (same pattern as the config normalizer).
1
+ // Shared usage ledger: append-only JSON lines, read by the CLI, `/tersio`, and
2
+ // the Dashboard. Best-effort; corrupt lines are skipped on read.
5
3
  import fs from 'node:fs';
6
4
  import os from 'node:os';
7
5
  import path from 'node:path';
8
- import { tersioDataPath } from '../lib/utils.js';
6
+ import { isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from '../lib/utils.js';
7
+ import { execFileSync } from 'node:child_process';
9
8
  export function ledgerPath() {
10
9
  const override = process.env.TERSIO_USAGE_FILE;
11
10
  if (override)
12
11
  return override;
13
12
  return tersioDataPath('usage.jsonl', 'tersio-usage.jsonl');
14
13
  }
15
- // Reset watermark: tersio-owned timestamp marking the last statistics reset.
16
- // Session transcripts and the RTK database are host/tool-owned and never
17
- // touched — instead, every derived view (token stats, command tools) filters
18
- // rows from before the watermark, so "reset" empties what tersio shows
19
- // without deleting anything it does not own.
14
+ // Reset watermark: views filter rows older than it, so nothing else is deleted.
20
15
  export function resetMarkerPath() {
21
16
  const override = process.env.TERSIO_RESET_FILE;
22
17
  if (override)
@@ -54,10 +49,8 @@ function zeroBreakdown() {
54
49
  export function durOf(v) {
55
50
  return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
56
51
  }
57
- // Measured cost for one message. OMP writes usage.cost as
58
- // { input, output, cacheRead, cacheWrite, total }; a bare number shows up in
59
- // older fixtures. Anything else means "not recorded" — deliberately not 0, so
60
- // the dashboard can tell a genuinely free run from an unrecorded one.
52
+ // Measured cost: usage.cost.total, or a bare number in old fixtures. Anything
53
+ // else means unrecorded, which is deliberately not 0.
61
54
  export function costOf(usage) {
62
55
  const c = usage.cost;
63
56
  if (typeof c === 'number')
@@ -95,7 +88,11 @@ export function sessionsDir() {
95
88
  const override = process.env.TERSIO_SESSIONS_DIR;
96
89
  if (override)
97
90
  return override;
98
- return path.join(os.homedir(), '.omp', 'agent', 'sessions');
91
+ // The running host wins; the other host's directory is the fallback.
92
+ const dirs = isPiProcess()
93
+ ? [path.join(piAgentDir(), 'sessions'), path.join(os.homedir(), '.omp', 'agent', 'sessions')]
94
+ : [path.join(os.homedir(), '.omp', 'agent', 'sessions'), path.join(piAgentDir(), 'sessions')];
95
+ return dirs.find((dir) => fs.existsSync(dir)) ?? dirs[0];
99
96
  }
100
97
  export function codexSessionsDir() {
101
98
  const override = process.env.TERSIO_CODEX_DIR;
@@ -132,11 +129,15 @@ export function walkJsonl(dir, out, cap) {
132
129
  out.push(full);
133
130
  }
134
131
  }
135
- // Recent requests are their own full-width table in the dashboard, so this is
136
- // a payload bound rather than a "few highlights" bound. Rows are small
137
- // (model, tokens, timing, status), so a few hundred cost little to ship.
132
+ // Recent requests get their own full-width table, so this bounds payload
133
+ // rather than highlights; rows are small, so a few hundred cost little.
138
134
  export const RECENT_LIMIT = 200;
139
135
  const FREE_SUFFIX = /(?::free|-free)$/i;
136
+ const RTK_ELIGIBLE_HEADS = new Set([
137
+ 'rtk', 'git', 'grep', 'rg', 'find', 'cat', 'ls', 'tree', 'diff', 'log',
138
+ 'npm', 'npx', 'pnpm', 'bun', 'bunx', 'cargo', 'go', 'python', 'pytest',
139
+ 'ruff', 'mypy', 'docker', 'kubectl', 'psql', 'aws', 'gh', 'glab', 'wc',
140
+ ]);
140
141
  export function newSessionAccum() {
141
142
  return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [] };
142
143
  }
@@ -194,19 +195,23 @@ export function classifySessionLine(row) {
194
195
  const computedDur = (typeof msg.completedAt === 'number' && typeof msg.timestamp === 'number') ? msg.completedAt - msg.timestamp : undefined;
195
196
  const tools = [];
196
197
  for (const part of msg.content ?? []) {
197
- if (part?.type === 'toolCall' && typeof part.name === 'string' && part.name) {
198
- tools.push(part.name === 'bash' ? `bash:${leadBinary(part.arguments?.command)}` : part.name);
198
+ if (part?.type !== 'toolCall' || typeof part.name !== 'string' || !part.name)
199
+ continue;
200
+ if (part.name !== 'bash') {
201
+ tools.push(part.name);
202
+ continue;
199
203
  }
204
+ const command = part.arguments?.command;
205
+ tools.push(`bash:${leadBinary(command)}`);
200
206
  }
201
207
  return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
202
208
  }
203
- // Fold `:free`/`-free` suffixes and case variants into one chart key, so the
204
- // same model from two providers stops splitting into separate rows.
209
+ // Fold `:free`/`-free` suffixes and case variants into one chart key.
205
210
  export function canonicalModelId(model) {
206
- return model.replace(FREE_SUFFIX, '').toLowerCase();
211
+ return canonicalPriceId(model.replace(FREE_SUFFIX, ''));
207
212
  }
208
- // LiteLLM-style display: namespace stays lowercase, model segments title-case
209
- // with version dots kept (`deepseek-v4.1-flash` → `Deepseek-V4.1-Flash`).
213
+ // LiteLLM-style display: lowercase namespace, title-cased model segments with
214
+ // version dots kept.
210
215
  export function displayModelId(model) {
211
216
  const bare = model.replace(FREE_SUFFIX, '');
212
217
  const cap = (s) => {
@@ -232,8 +237,8 @@ export function importSessionTokens() {
232
237
  const accum = newSessionAccum();
233
238
  const files = [];
234
239
  walkJsonl(sessionsDir(), files, 2000);
235
- // A sessions override signals an isolated environment (tests, fixtures):
236
- // only walk the real codex dir when it is explicitly set.
240
+ // An override means an isolated environment (tests, fixtures): only walk the
241
+ // real codex dir when one is explicitly set.
237
242
  if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
238
243
  walkJsonl(codexSessionsDir(), files, 2000);
239
244
  }
@@ -251,6 +256,82 @@ export function importSessionTokens() {
251
256
  accum.recent.sort((a, b) => b.t - a.t);
252
257
  return { messages: accum.messages, totals: accum.totals, byModel: accum.byModel, byDay: accum.byDay, byDayModel: accum.byDayModel, byTool: accum.byTool, byModelMessages: accum.byModelMessages, costMeasured: accum.costMeasured, recent: accum.recent.slice(0, RECENT_LIMIT) };
253
258
  }
259
+ const adoptionFileCache = new Map();
260
+ function parseAdoptionFile(file) {
261
+ const counts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
262
+ let text;
263
+ try {
264
+ text = fs.readFileSync(file, 'utf8');
265
+ }
266
+ catch {
267
+ return { counts, sessions: 0 };
268
+ }
269
+ let sawBash = false;
270
+ for (const line of text.split('\n')) {
271
+ if (!line.includes('tool_execution_start'))
272
+ continue;
273
+ try {
274
+ const row = JSON.parse(line);
275
+ if (row.type !== 'custom' || row.customType !== 'tool_execution_start' || row.data?.toolName !== 'bash')
276
+ continue;
277
+ const command = row.data.args?.command;
278
+ if (typeof command !== 'string')
279
+ continue;
280
+ sawBash = true;
281
+ counts.bashCalls += 1;
282
+ const head = leadBinary(command);
283
+ if (RTK_ELIGIBLE_HEADS.has(head))
284
+ counts.eligibleCalls += 1;
285
+ if (head === 'rtk')
286
+ counts.rtkCalls += 1;
287
+ }
288
+ catch { /* skip corrupt line */ }
289
+ }
290
+ return { counts, sessions: sawBash ? 1 : 0 };
291
+ }
292
+ export function clearRtkAdoptionCache() {
293
+ adoptionFileCache.clear();
294
+ }
295
+ export function readRtkAdoption() {
296
+ const files = [];
297
+ walkJsonl(sessionsDir(), files, 2000);
298
+ if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
299
+ walkJsonl(codexSessionsDir(), files, 2000);
300
+ }
301
+ const live = new Set(files);
302
+ for (const file of adoptionFileCache.keys()) {
303
+ if (!live.has(file))
304
+ adoptionFileCache.delete(file);
305
+ }
306
+ let bashCalls = 0;
307
+ let eligibleCalls = 0;
308
+ let rtkCalls = 0;
309
+ let sessions = 0;
310
+ for (const file of files) {
311
+ let stat;
312
+ try {
313
+ stat = fs.statSync(file);
314
+ }
315
+ catch {
316
+ continue;
317
+ }
318
+ let entry = adoptionFileCache.get(file);
319
+ if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
320
+ const parsed = parseAdoptionFile(file);
321
+ entry = { size: stat.size, mtimeMs: stat.mtimeMs, counts: parsed.counts };
322
+ adoptionFileCache.set(file, entry);
323
+ sessions += parsed.sessions;
324
+ }
325
+ else if (entry.counts.bashCalls > 0) {
326
+ sessions += 1;
327
+ }
328
+ bashCalls += entry.counts.bashCalls;
329
+ eligibleCalls += entry.counts.eligibleCalls;
330
+ rtkCalls += entry.counts.rtkCalls;
331
+ }
332
+ const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
333
+ return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
334
+ }
254
335
  export function processSessionText(accum, state, text) {
255
336
  for (const line of text.split('\n')) {
256
337
  if (!line.trim())
@@ -275,8 +356,31 @@ export function processSessionText(accum, state, text) {
275
356
  catch { /* skip corrupt lines */ }
276
357
  }
277
358
  }
278
- // Lead binary of a shell string: first segment head past `cd` chains and
279
- // VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
359
+ export function readRtkRecallDiagnostics(binary = resolveRtkBinary()) {
360
+ if (!binary)
361
+ return { mode: 'unknown', entries: 0, available: false };
362
+ try {
363
+ const configPath = process.env.RTK_CONFIG
364
+ || (process.platform === 'darwin'
365
+ ? path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'config.toml')
366
+ : path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'rtk', 'config.toml'));
367
+ let config = '';
368
+ try {
369
+ config = fs.readFileSync(configPath, 'utf8');
370
+ }
371
+ catch { /* use RTK CLI fallback */ }
372
+ const modeMatch = /^\s*mode\s*=\s*"(sqlite|tee|disabled)"/m.exec(config);
373
+ const mode = (modeMatch?.[1] ?? 'unknown');
374
+ const output = execFileSync(binary, ['recall', '--list'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
375
+ const entries = /\((\d+)\s+(?:entries?|stored)\)/i.exec(output)?.[1] ?? /^\s*(\d+)\s+entries?/im.exec(output)?.[1] ?? '0';
376
+ return { mode, entries: Number(entries) || 0, available: true };
377
+ }
378
+ catch {
379
+ return { mode: 'unknown', entries: 0, available: false };
380
+ }
381
+ }
382
+ // Lead binary of a shell string: first segment past `cd` chains and VAR=x
383
+ // assignments. Falls back to `bash`.
280
384
  const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
281
385
  export function leadBinary(command) {
282
386
  if (typeof command !== 'string' || !command.trim())
@@ -293,7 +397,7 @@ export function leadBinary(command) {
293
397
  return 'bash';
294
398
  }
295
399
  // --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
296
- import { DEFAULT_PRICE, priceFor, refreshPricesIfStale } from './pricing.js';
400
+ import { DEFAULT_PRICE, canonicalPriceId, priceFor, refreshPricesIfStale } from './pricing.js';
297
401
  import { co2GramsFor, energyWhFor } from './carbon.js';
298
402
  export { DEFAULT_PRICE, priceFor, refreshPricesIfStale };
299
403
  export { co2GramsFor, energyWhFor };
@@ -312,11 +416,6 @@ export function usdCost(t, model) {
312
416
  buckets,
313
417
  };
314
418
  }
315
- // CO2 now model-differentiated via ./carbon.ts (EcoLogits 0.8.2 port).
316
- // Always render with ~est. and never merge with measured figures.
317
- // The constant below is the served gCO2eq per 1K output tokens for the
318
- // default (gpt-4o-class) model, kept so single-figure callers stay honest.
319
- export const CO2_G_PER_1K_OUTPUT = 0.2;
320
419
  export function co2Grams(outputTokens, model) {
321
420
  return co2GramsFor(model ?? 'gpt-4o', outputTokens);
322
421
  }
@@ -342,9 +441,8 @@ export function readUsage() {
342
441
  }
343
442
  return rows;
344
443
  }
345
- // Delete the ledger file. Returns the rows cleared (0 when already absent).
346
- // Only tersio-owned data lives here; session transcripts and the RTK
347
- // database are never touched.
444
+ // Delete the ledger file and return the rows cleared. Tersio-owned data only:
445
+ // transcripts and the RTK database are never touched.
348
446
  export function clearUsageLedger() {
349
447
  const rows = readUsage().length;
350
448
  try {