@krtclcdy/tersio 2.23.0 → 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.
- package/README.md +58 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +16 -20
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +49 -37
- package/cli/doctor-fix.js +28 -17
- package/cli/doctor.js +52 -41
- package/cli/hosts.js +118 -0
- package/cli/install.js +180 -117
- package/cli/interactive.js +27 -1
- package/cli/layer.js +24 -0
- package/cli/manifest.js +36 -0
- package/cli/omp-layer.js +51 -0
- package/cli/pi-layer.js +37 -0
- package/cli/pi-wiring.js +92 -0
- package/cli/profile.js +44 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/uninstall.js +135 -44
- package/cli/update.js +12 -19
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +31 -24
- package/extensions/ai-addons-updater/index.ts +32 -32
- package/extensions/caveman-session/index.js +6 -6
- package/extensions/caveman-session/index.ts +6 -6
- package/extensions/combo-toggle/index.js +42 -40
- package/extensions/combo-toggle/index.ts +36 -47
- package/extensions/lib/utils.js +36 -9
- package/extensions/lib/utils.ts +37 -9
- package/extensions/rtk-session/index.js +9 -9
- package/extensions/rtk-session/index.ts +9 -14
- package/extensions/shared/carbon.js +5 -8
- package/extensions/shared/carbon.ts +5 -8
- package/extensions/shared/host.js +68 -0
- package/extensions/shared/host.ts +104 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +43 -30
- package/extensions/shared/plugin-settings.ts +45 -26
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +11 -22
- package/extensions/shared/rtk-gain.ts +11 -22
- package/extensions/shared/session-state.js +21 -25
- package/extensions/shared/session-state.ts +22 -26
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +24 -36
- package/extensions/shared/usage-ledger.ts +29 -50
- package/extensions/shared/usage-store.js +14 -12
- package/extensions/shared/usage-store.ts +16 -14
- package/extensions/tersio-commands/index.js +2 -1
- package/extensions/tersio-commands/index.ts +2 -1
- package/package.json +4 -2
- package/tersio.js +4 -4
- package/extensions/shared/mode-reinforcement.js +0 -26
|
@@ -76,23 +76,22 @@ export function normalizeComboLevel(value) {
|
|
|
76
76
|
const level = String(value || '').trim().toLowerCase();
|
|
77
77
|
return isPresetLevel(level) ? level : null;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// OMP themes status text through ctx.ui.theme.fg; pi has no ctx.ui.theme, so
|
|
80
|
+
// the same string renders plain there.
|
|
81
|
+
export function themeStatus(ui, indicator, label, highlight = false) {
|
|
80
82
|
const theme = ui?.theme;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// made by a sibling extension reaches this bar. The symptom is a bar frozen on
|
|
94
|
-
// a preset the session no longer has: it shows "combo BALANCED" while caveman
|
|
95
|
-
// and rtk are already off.
|
|
83
|
+
if (!theme?.fg)
|
|
84
|
+
return `${indicator} ${label}`;
|
|
85
|
+
return `${highlight ? theme.fg('accent', indicator) : indicator} ${theme.fg('muted', label)}`;
|
|
86
|
+
}
|
|
87
|
+
export function paintStatusBar(ui, key, emoji, label, isActive) {
|
|
88
|
+
ui?.setStatus?.(key, themeStatus(ui, emoji, label, isActive));
|
|
89
|
+
}
|
|
90
|
+
// Which context a status bar paints through. A UI-less event (a headless child
|
|
91
|
+
// session_start, or a top-level one that fires before the TUI attaches) must
|
|
92
|
+
// never be remembered: doing so mutes every later bare syncStatus(), which is
|
|
93
|
+
// the bridge-listener path a sibling's mode change arrives on. The symptom is a
|
|
94
|
+
// bar frozen on a preset the session no longer has.
|
|
96
95
|
export function paintableCtx(remembered, next) {
|
|
97
96
|
return next?.ui?.setStatus ? next : remembered;
|
|
98
97
|
}
|
|
@@ -131,8 +130,7 @@ export function setSharedComboMode(name, value) {
|
|
|
131
130
|
return getSharedComboState();
|
|
132
131
|
const modes = { ...getSharedComboState(), [name]: mode };
|
|
133
132
|
// Derive the level from the final triplet so a redundant same-value write
|
|
134
|
-
//
|
|
135
|
-
// strand a matching preset at 'custom' and drop the combo bar.
|
|
133
|
+
// cannot strand a matching preset at 'custom' and drop the combo bar.
|
|
136
134
|
return publish(normalizedState(modes));
|
|
137
135
|
}
|
|
138
136
|
export function reconcileSharedComboEntries(entries) {
|
|
@@ -156,19 +154,17 @@ export function reconcileSharedComboEntries(entries) {
|
|
|
156
154
|
modes[name] = mode;
|
|
157
155
|
}
|
|
158
156
|
}
|
|
159
|
-
// The level
|
|
160
|
-
//
|
|
157
|
+
// The level reflects the final triplet, never the write order, so a preset
|
|
158
|
+
// entry followed by the same values reconciles back to the preset.
|
|
161
159
|
return publish(normalizedState(modes));
|
|
162
160
|
}
|
|
163
|
-
// One-line session-wide mode summary for change confirmations
|
|
164
|
-
// `caveman=ULTRA, rtk=ON, ponytail=ULTRA`.
|
|
161
|
+
// One-line session-wide mode summary for change confirmations.
|
|
165
162
|
export function activeModesSummary(state) {
|
|
166
163
|
return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
|
|
167
164
|
}
|
|
168
165
|
export function setSharedComboListener(listener) {
|
|
169
|
-
// Additive: every sibling
|
|
170
|
-
//
|
|
171
|
-
// A null listener clears all (no current callers; reserved for teardown).
|
|
166
|
+
// Additive: every sibling syncs its mirror on publish, so a /tersio or
|
|
167
|
+
// /combo switch lands next turn with no reload. A null listener clears all.
|
|
172
168
|
if (typeof listener === 'function')
|
|
173
169
|
bridge().listeners.add(listener);
|
|
174
170
|
else
|
|
@@ -98,24 +98,23 @@ export function normalizeComboLevel(value: unknown): ComboLevel | null {
|
|
|
98
98
|
return isPresetLevel(level) ? level : null;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
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 {
|
|
102
104
|
const theme = ui?.theme;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// made by a sibling extension reaches this bar. The symptom is a bar frozen on
|
|
117
|
-
// a preset the session no longer has: it shows "combo BALANCED" while caveman
|
|
118
|
-
// 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.
|
|
119
118
|
export function paintableCtx(remembered: ExtensionCtx | undefined, next: ExtensionCtx | undefined): ExtensionCtx | undefined {
|
|
120
119
|
return next?.ui?.setStatus ? next : remembered;
|
|
121
120
|
}
|
|
@@ -156,8 +155,7 @@ export function setSharedComboMode(name: ModeName, value: unknown): Readonly<Com
|
|
|
156
155
|
if (!mode) return getSharedComboState();
|
|
157
156
|
const modes = { ...getSharedComboState(), [name]: mode } as Modes;
|
|
158
157
|
// Derive the level from the final triplet so a redundant same-value write
|
|
159
|
-
//
|
|
160
|
-
// strand a matching preset at 'custom' and drop the combo bar.
|
|
158
|
+
// cannot strand a matching preset at 'custom' and drop the combo bar.
|
|
161
159
|
return publish(normalizedState(modes));
|
|
162
160
|
}
|
|
163
161
|
|
|
@@ -178,21 +176,19 @@ export function reconcileSharedComboEntries(entries: SessionEntry[] | null | und
|
|
|
178
176
|
if (mode) modes[name] = mode;
|
|
179
177
|
}
|
|
180
178
|
}
|
|
181
|
-
// The level
|
|
182
|
-
//
|
|
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.
|
|
183
181
|
return publish(normalizedState(modes));
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
// One-line session-wide mode summary for change confirmations
|
|
187
|
-
// `caveman=ULTRA, rtk=ON, ponytail=ULTRA`.
|
|
184
|
+
// One-line session-wide mode summary for change confirmations.
|
|
188
185
|
export function activeModesSummary(state: { caveman: string; rtk: string; ponytail: string }): string {
|
|
189
186
|
return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
|
|
190
187
|
}
|
|
191
188
|
|
|
192
189
|
export function setSharedComboListener(listener: ((state: Readonly<ComboState>) => void) | null): void {
|
|
193
|
-
// Additive: every sibling
|
|
194
|
-
//
|
|
195
|
-
// 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.
|
|
196
192
|
if (typeof listener === 'function') bridge().listeners.add(listener);
|
|
197
193
|
else bridge().listeners.clear();
|
|
198
194
|
}
|
|
@@ -47,8 +47,16 @@ export interface ExtensionCtx {
|
|
|
47
47
|
|
|
48
48
|
export interface SystemPromptEvent {
|
|
49
49
|
systemPrompt: string | string[];
|
|
50
|
+
// pi only: the mutable prompt sections. OMP does not provide it.
|
|
51
|
+
systemPromptOptions?: { appendSystemPrompt?: string };
|
|
50
52
|
}
|
|
51
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
|
+
|
|
52
60
|
export interface InputEvent {
|
|
53
61
|
text?: string;
|
|
54
62
|
source?: string;
|
|
@@ -68,7 +76,7 @@ export interface ExtensionApi {
|
|
|
68
76
|
execute: (
|
|
69
77
|
toolCallId: string,
|
|
70
78
|
params: { args: string[] },
|
|
71
|
-
signal: AbortSignal,
|
|
79
|
+
signal: AbortSignal | undefined,
|
|
72
80
|
onUpdate: ((data: unknown) => void) | undefined,
|
|
73
81
|
ctx: ExtensionCtx | undefined,
|
|
74
82
|
) => Promise<{
|
|
@@ -86,5 +94,4 @@ export interface ExtensionApi {
|
|
|
86
94
|
}>;
|
|
87
95
|
cwd?: string;
|
|
88
96
|
zod?: { z: unknown };
|
|
89
|
-
on<E>(event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void;
|
|
90
|
-
}
|
|
97
|
+
on<E>(event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void;}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
// Shared usage ledger: append-only JSON
|
|
2
|
-
//
|
|
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 { resolveRtkBinary, tersioDataPath } from '../lib/utils.js';
|
|
6
|
+
import { isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from '../lib/utils.js';
|
|
9
7
|
import { execFileSync } from 'node:child_process';
|
|
10
8
|
export function ledgerPath() {
|
|
11
9
|
const override = process.env.TERSIO_USAGE_FILE;
|
|
@@ -13,11 +11,7 @@ export function ledgerPath() {
|
|
|
13
11
|
return override;
|
|
14
12
|
return tersioDataPath('usage.jsonl', 'tersio-usage.jsonl');
|
|
15
13
|
}
|
|
16
|
-
// Reset watermark:
|
|
17
|
-
// Session transcripts and the RTK database are host/tool-owned and never
|
|
18
|
-
// touched — instead, every derived view (token stats, command tools) filters
|
|
19
|
-
// rows from before the watermark, so "reset" empties what tersio shows
|
|
20
|
-
// without deleting anything it does not own.
|
|
14
|
+
// Reset watermark: views filter rows older than it, so nothing else is deleted.
|
|
21
15
|
export function resetMarkerPath() {
|
|
22
16
|
const override = process.env.TERSIO_RESET_FILE;
|
|
23
17
|
if (override)
|
|
@@ -55,10 +49,8 @@ function zeroBreakdown() {
|
|
|
55
49
|
export function durOf(v) {
|
|
56
50
|
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
57
51
|
}
|
|
58
|
-
// Measured cost
|
|
59
|
-
//
|
|
60
|
-
// older fixtures. Anything else means "not recorded" — deliberately not 0, so
|
|
61
|
-
// 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.
|
|
62
54
|
export function costOf(usage) {
|
|
63
55
|
const c = usage.cost;
|
|
64
56
|
if (typeof c === 'number')
|
|
@@ -96,7 +88,11 @@ export function sessionsDir() {
|
|
|
96
88
|
const override = process.env.TERSIO_SESSIONS_DIR;
|
|
97
89
|
if (override)
|
|
98
90
|
return override;
|
|
99
|
-
|
|
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];
|
|
100
96
|
}
|
|
101
97
|
export function codexSessionsDir() {
|
|
102
98
|
const override = process.env.TERSIO_CODEX_DIR;
|
|
@@ -133,9 +129,8 @@ export function walkJsonl(dir, out, cap) {
|
|
|
133
129
|
out.push(full);
|
|
134
130
|
}
|
|
135
131
|
}
|
|
136
|
-
// Recent requests
|
|
137
|
-
//
|
|
138
|
-
// (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.
|
|
139
134
|
export const RECENT_LIMIT = 200;
|
|
140
135
|
const FREE_SUFFIX = /(?::free|-free)$/i;
|
|
141
136
|
const RTK_ELIGIBLE_HEADS = new Set([
|
|
@@ -211,13 +206,12 @@ export function classifySessionLine(row) {
|
|
|
211
206
|
}
|
|
212
207
|
return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
|
|
213
208
|
}
|
|
214
|
-
// Fold `:free`/`-free` suffixes and case variants into one chart key
|
|
215
|
-
// same model from two providers stops splitting into separate rows.
|
|
209
|
+
// Fold `:free`/`-free` suffixes and case variants into one chart key.
|
|
216
210
|
export function canonicalModelId(model) {
|
|
217
|
-
return model.replace(FREE_SUFFIX, '')
|
|
211
|
+
return canonicalPriceId(model.replace(FREE_SUFFIX, ''));
|
|
218
212
|
}
|
|
219
|
-
// LiteLLM-style display: namespace
|
|
220
|
-
//
|
|
213
|
+
// LiteLLM-style display: lowercase namespace, title-cased model segments with
|
|
214
|
+
// version dots kept.
|
|
221
215
|
export function displayModelId(model) {
|
|
222
216
|
const bare = model.replace(FREE_SUFFIX, '');
|
|
223
217
|
const cap = (s) => {
|
|
@@ -243,8 +237,8 @@ export function importSessionTokens() {
|
|
|
243
237
|
const accum = newSessionAccum();
|
|
244
238
|
const files = [];
|
|
245
239
|
walkJsonl(sessionsDir(), files, 2000);
|
|
246
|
-
//
|
|
247
|
-
//
|
|
240
|
+
// An override means an isolated environment (tests, fixtures): only walk the
|
|
241
|
+
// real codex dir when one is explicitly set.
|
|
248
242
|
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
|
|
249
243
|
walkJsonl(codexSessionsDir(), files, 2000);
|
|
250
244
|
}
|
|
@@ -385,8 +379,8 @@ export function readRtkRecallDiagnostics(binary = resolveRtkBinary()) {
|
|
|
385
379
|
return { mode: 'unknown', entries: 0, available: false };
|
|
386
380
|
}
|
|
387
381
|
}
|
|
388
|
-
// Lead binary of a shell string: first segment
|
|
389
|
-
//
|
|
382
|
+
// Lead binary of a shell string: first segment past `cd` chains and VAR=x
|
|
383
|
+
// assignments. Falls back to `bash`.
|
|
390
384
|
const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
|
|
391
385
|
export function leadBinary(command) {
|
|
392
386
|
if (typeof command !== 'string' || !command.trim())
|
|
@@ -403,7 +397,7 @@ export function leadBinary(command) {
|
|
|
403
397
|
return 'bash';
|
|
404
398
|
}
|
|
405
399
|
// --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
|
|
406
|
-
import { DEFAULT_PRICE, priceFor, refreshPricesIfStale } from './pricing.js';
|
|
400
|
+
import { DEFAULT_PRICE, canonicalPriceId, priceFor, refreshPricesIfStale } from './pricing.js';
|
|
407
401
|
import { co2GramsFor, energyWhFor } from './carbon.js';
|
|
408
402
|
export { DEFAULT_PRICE, priceFor, refreshPricesIfStale };
|
|
409
403
|
export { co2GramsFor, energyWhFor };
|
|
@@ -422,11 +416,6 @@ export function usdCost(t, model) {
|
|
|
422
416
|
buckets,
|
|
423
417
|
};
|
|
424
418
|
}
|
|
425
|
-
// CO2 now model-differentiated via ./carbon.ts (EcoLogits 0.8.2 port).
|
|
426
|
-
// Always render with ~est. and never merge with measured figures.
|
|
427
|
-
// The constant below is the served gCO2eq per 1K output tokens for the
|
|
428
|
-
// default (gpt-4o-class) model, kept so single-figure callers stay honest.
|
|
429
|
-
export const CO2_G_PER_1K_OUTPUT = 0.2;
|
|
430
419
|
export function co2Grams(outputTokens, model) {
|
|
431
420
|
return co2GramsFor(model ?? 'gpt-4o', outputTokens);
|
|
432
421
|
}
|
|
@@ -452,9 +441,8 @@ export function readUsage() {
|
|
|
452
441
|
}
|
|
453
442
|
return rows;
|
|
454
443
|
}
|
|
455
|
-
// Delete the ledger file
|
|
456
|
-
//
|
|
457
|
-
// 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.
|
|
458
446
|
export function clearUsageLedger() {
|
|
459
447
|
const rows = readUsage().length;
|
|
460
448
|
try {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
// Shared usage ledger: append-only JSON
|
|
2
|
-
//
|
|
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 { resolveRtkBinary, tersioDataPath } from '../lib/utils.ts';
|
|
6
|
+
import { isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from '../lib/utils.ts';
|
|
9
7
|
import { execFileSync } from 'node:child_process';
|
|
10
8
|
|
|
11
9
|
export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
|
|
@@ -22,11 +20,7 @@ export function ledgerPath(): string {
|
|
|
22
20
|
return tersioDataPath('usage.jsonl', 'tersio-usage.jsonl');
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
// Reset watermark:
|
|
26
|
-
// Session transcripts and the RTK database are host/tool-owned and never
|
|
27
|
-
// touched — instead, every derived view (token stats, command tools) filters
|
|
28
|
-
// rows from before the watermark, so "reset" empties what tersio shows
|
|
29
|
-
// without deleting anything it does not own.
|
|
23
|
+
// Reset watermark: views filter rows older than it, so nothing else is deleted.
|
|
30
24
|
export function resetMarkerPath(): string {
|
|
31
25
|
const override = process.env.TERSIO_RESET_FILE;
|
|
32
26
|
if (override) return override;
|
|
@@ -59,10 +53,7 @@ export function appendUsage(kind: UsageKind, detail: string): void {
|
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
// --- Session tokens, tokscale-style ------------------------------------------
|
|
62
|
-
//
|
|
63
|
-
// message.role === 'assistant', message.usage = { input, output, cacheRead,
|
|
64
|
-
// cacheWrite }, message.model, plus a per-line timestamp. Same source tokscale
|
|
65
|
-
// parses for its oh-my-pi client. Best-effort: unreadable files are skipped.
|
|
56
|
+
// Assistant messages in the host transcripts carry usage, model, and a timestamp.
|
|
66
57
|
export interface TokenBreakdown {
|
|
67
58
|
input: number;
|
|
68
59
|
output: number;
|
|
@@ -70,10 +61,7 @@ export interface TokenBreakdown {
|
|
|
70
61
|
cacheWrite: number;
|
|
71
62
|
}
|
|
72
63
|
|
|
73
|
-
//
|
|
74
|
-
// (toolUse | stop | aborted | error) and, when the request failed, an HTTP
|
|
75
|
-
// errorStatus plus errorMessage. `toolUse` is an ordinary turn that handed off
|
|
76
|
-
// to tools, so it reads as completed rather than as a distinct state.
|
|
64
|
+
// `toolUse` is an ordinary turn that handed off to tools, so it reads completed.
|
|
77
65
|
export type RunStatus = 'completed' | 'aborted' | 'error';
|
|
78
66
|
|
|
79
67
|
export interface RecentRequest {
|
|
@@ -84,9 +72,7 @@ export interface RecentRequest {
|
|
|
84
72
|
d?: number;
|
|
85
73
|
cr?: number;
|
|
86
74
|
cw?: number;
|
|
87
|
-
//
|
|
88
|
-
// Left undefined when the host recorded no cost — never backfilled with an
|
|
89
|
-
// estimate, so a measured figure is never mistaken for a modeled one.
|
|
75
|
+
// Undefined when the host recorded no cost; never backfilled.
|
|
90
76
|
usd?: number;
|
|
91
77
|
st: RunStatus;
|
|
92
78
|
code?: number;
|
|
@@ -127,10 +113,8 @@ export function durOf(v: unknown): number | undefined {
|
|
|
127
113
|
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
128
114
|
}
|
|
129
115
|
|
|
130
|
-
// Measured cost
|
|
131
|
-
//
|
|
132
|
-
// older fixtures. Anything else means "not recorded" — deliberately not 0, so
|
|
133
|
-
// the dashboard can tell a genuinely free run from an unrecorded one.
|
|
116
|
+
// Measured cost: usage.cost.total, or a bare number in old fixtures. Anything
|
|
117
|
+
// else means unrecorded, which is deliberately not 0.
|
|
134
118
|
export function costOf(usage: Record<string, unknown>): number | undefined {
|
|
135
119
|
const c = usage.cost;
|
|
136
120
|
if (typeof c === 'number') return Number.isFinite(c) ? c : undefined;
|
|
@@ -166,7 +150,11 @@ function addInto(into: TokenBreakdown, u: { input?: unknown; output?: unknown; c
|
|
|
166
150
|
export function sessionsDir(): string {
|
|
167
151
|
const override = process.env.TERSIO_SESSIONS_DIR;
|
|
168
152
|
if (override) return override;
|
|
169
|
-
|
|
153
|
+
// The running host wins; the other host's directory is the fallback.
|
|
154
|
+
const dirs = isPiProcess()
|
|
155
|
+
? [path.join(piAgentDir(), 'sessions'), path.join(os.homedir(), '.omp', 'agent', 'sessions')]
|
|
156
|
+
: [path.join(os.homedir(), '.omp', 'agent', 'sessions'), path.join(piAgentDir(), 'sessions')];
|
|
157
|
+
return dirs.find((dir) => fs.existsSync(dir)) ?? dirs[0];
|
|
170
158
|
}
|
|
171
159
|
|
|
172
160
|
export function codexSessionsDir(): string {
|
|
@@ -198,9 +186,8 @@ export function walkJsonl(dir: string, out: string[], cap: number): void {
|
|
|
198
186
|
else if (e.isFile() && e.name.endsWith('.jsonl')) out.push(full);
|
|
199
187
|
}
|
|
200
188
|
}
|
|
201
|
-
// Recent requests
|
|
202
|
-
//
|
|
203
|
-
// (model, tokens, timing, status), so a few hundred cost little to ship.
|
|
189
|
+
// Recent requests get their own full-width table, so this bounds payload
|
|
190
|
+
// rather than highlights; rows are small, so a few hundred cost little.
|
|
204
191
|
export const RECENT_LIMIT = 200;
|
|
205
192
|
const FREE_SUFFIX = /(?::free|-free)$/i;
|
|
206
193
|
const RTK_ELIGIBLE_HEADS = new Set([
|
|
@@ -208,8 +195,8 @@ const RTK_ELIGIBLE_HEADS = new Set([
|
|
|
208
195
|
'npm', 'npx', 'pnpm', 'bun', 'bunx', 'cargo', 'go', 'python', 'pytest',
|
|
209
196
|
'ruff', 'mypy', 'docker', 'kubectl', 'psql', 'aws', 'gh', 'glab', 'wc',
|
|
210
197
|
]);
|
|
211
|
-
//
|
|
212
|
-
//
|
|
198
|
+
// Parse buffer shared by live reads and usage.db syncs, so the store is a
|
|
199
|
+
// cache and never a fork.
|
|
213
200
|
export interface SessionAccum {
|
|
214
201
|
byModel: Record<string, TokenBreakdown>;
|
|
215
202
|
byDay: Record<string, TokenBreakdown>;
|
|
@@ -284,13 +271,12 @@ export function classifySessionLine(row: { timestamp?: string | number; type?: u
|
|
|
284
271
|
}
|
|
285
272
|
return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
|
|
286
273
|
}
|
|
287
|
-
// Fold `:free`/`-free` suffixes and case variants into one chart key
|
|
288
|
-
// same model from two providers stops splitting into separate rows.
|
|
274
|
+
// Fold `:free`/`-free` suffixes and case variants into one chart key.
|
|
289
275
|
export function canonicalModelId(model: string): string {
|
|
290
|
-
return model.replace(FREE_SUFFIX, '')
|
|
276
|
+
return canonicalPriceId(model.replace(FREE_SUFFIX, ''));
|
|
291
277
|
}
|
|
292
|
-
// LiteLLM-style display: namespace
|
|
293
|
-
//
|
|
278
|
+
// LiteLLM-style display: lowercase namespace, title-cased model segments with
|
|
279
|
+
// version dots kept.
|
|
294
280
|
export function displayModelId(model: string): string {
|
|
295
281
|
const bare = model.replace(FREE_SUFFIX, '');
|
|
296
282
|
const cap = (s: string): string => {
|
|
@@ -312,8 +298,8 @@ export function importSessionTokens(): SessionTokens {
|
|
|
312
298
|
const accum = newSessionAccum();
|
|
313
299
|
const files: string[] = [];
|
|
314
300
|
walkJsonl(sessionsDir(), files, 2000);
|
|
315
|
-
//
|
|
316
|
-
//
|
|
301
|
+
// An override means an isolated environment (tests, fixtures): only walk the
|
|
302
|
+
// real codex dir when one is explicitly set.
|
|
317
303
|
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
|
|
318
304
|
walkJsonl(codexSessionsDir(), files, 2000);
|
|
319
305
|
}
|
|
@@ -443,8 +429,8 @@ export function readRtkRecallDiagnostics(binary: string | null = resolveRtkBinar
|
|
|
443
429
|
return { mode: 'unknown', entries: 0, available: false };
|
|
444
430
|
}
|
|
445
431
|
}
|
|
446
|
-
// Lead binary of a shell string: first segment
|
|
447
|
-
//
|
|
432
|
+
// Lead binary of a shell string: first segment past `cd` chains and VAR=x
|
|
433
|
+
// assignments. Falls back to `bash`.
|
|
448
434
|
const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
|
|
449
435
|
export function leadBinary(command: unknown): string {
|
|
450
436
|
if (typeof command !== 'string' || !command.trim()) return 'bash';
|
|
@@ -459,7 +445,7 @@ export function leadBinary(command: unknown): string {
|
|
|
459
445
|
}
|
|
460
446
|
|
|
461
447
|
// --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
|
|
462
|
-
import { DEFAULT_PRICE, priceFor, refreshPricesIfStale } from './pricing.ts';
|
|
448
|
+
import { DEFAULT_PRICE, canonicalPriceId, priceFor, refreshPricesIfStale } from './pricing.ts';
|
|
463
449
|
import { co2GramsFor, energyWhFor } from './carbon.ts';
|
|
464
450
|
import type { ModelPrice } from './pricing.ts';
|
|
465
451
|
|
|
@@ -483,12 +469,6 @@ export function usdCost(t: TokenBreakdown, model?: string): { usd: number; price
|
|
|
483
469
|
};
|
|
484
470
|
}
|
|
485
471
|
|
|
486
|
-
// CO2 now model-differentiated via ./carbon.ts (EcoLogits 0.8.2 port).
|
|
487
|
-
// Always render with ~est. and never merge with measured figures.
|
|
488
|
-
// The constant below is the served gCO2eq per 1K output tokens for the
|
|
489
|
-
// default (gpt-4o-class) model, kept so single-figure callers stay honest.
|
|
490
|
-
export const CO2_G_PER_1K_OUTPUT = 0.2;
|
|
491
|
-
|
|
492
472
|
export function co2Grams(outputTokens: number, model?: string): number {
|
|
493
473
|
return co2GramsFor(model ?? 'gpt-4o', outputTokens);
|
|
494
474
|
}
|
|
@@ -513,9 +493,8 @@ export function readUsage(): UsageRow[] {
|
|
|
513
493
|
return rows;
|
|
514
494
|
}
|
|
515
495
|
|
|
516
|
-
// Delete the ledger file
|
|
517
|
-
//
|
|
518
|
-
// database are never touched.
|
|
496
|
+
// Delete the ledger file and return the rows cleared. Tersio-owned data only:
|
|
497
|
+
// transcripts and the RTK database are never touched.
|
|
519
498
|
export function clearUsageLedger(): number {
|
|
520
499
|
const rows = readUsage().length;
|
|
521
500
|
try {
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
// extensions/shared/usage-store.ts — tersio-owned usage.db.
|
|
2
|
-
// Persists the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// never a fork. Missing sqlite3 CLI → sync false / read null, callers fall
|
|
8
|
-
// back to the live path. Never touches RTK's history.db or host transcripts.
|
|
2
|
+
// Persists the rows importSessionTokens derives live, so reports survive
|
|
3
|
+
// session-file rotation. Sync is incremental (a files ledger skips unchanged
|
|
4
|
+
// transcripts) and every read applies the reset watermark, so the store is a
|
|
5
|
+
// cache, never a fork. Missing sqlite3 → sync false / read null. Never touches
|
|
6
|
+
// RTK's history.db or host transcripts.
|
|
9
7
|
import { execFileSync } from 'node:child_process';
|
|
10
8
|
import fs from 'node:fs';
|
|
11
9
|
import path from 'node:path';
|
|
12
10
|
import { RECENT_LIMIT, canonicalModelId, classifySessionLine, codexSessionsDir, costOf, durOf, ingestSessionRow, newSessionAccum, sessionsDir, walkJsonl, } from './usage-ledger.js';
|
|
13
11
|
import { tersioDataPath } from '../lib/utils.js';
|
|
12
|
+
import { MODEL_ALIASES } from './pricing.js';
|
|
14
13
|
export function usageDbPath() {
|
|
15
14
|
const override = process.env.TERSIO_USAGE_DB;
|
|
16
15
|
if (override)
|
|
@@ -45,12 +44,16 @@ function query(db, sql) {
|
|
|
45
44
|
}
|
|
46
45
|
function ensureSchema(db) {
|
|
47
46
|
fs.mkdirSync(path.dirname(db), { recursive: true });
|
|
47
|
+
// Rows imported before an alias existed keep the spelling they were stored
|
|
48
|
+
// with, and the mtime ledger will not re-read those transcripts. Fold them in
|
|
49
|
+
// once, so the merge shows up without a reset.
|
|
50
|
+
const rekeys = MODEL_ALIASES.map((a) => `UPDATE messages SET model='${a.id}' WHERE model='${a.feed}';`).join('');
|
|
48
51
|
run(db, `CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);` +
|
|
49
52
|
`CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, mtime REAL NOT NULL, size INTEGER NOT NULL);` +
|
|
50
53
|
`CREATE TABLE IF NOT EXISTS messages (file TEXT NOT NULL, t REAL, model TEXT NOT NULL,` +
|
|
51
54
|
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
52
55
|
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]');` +
|
|
53
|
-
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file);`);
|
|
56
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file);` + rekeys);
|
|
54
57
|
}
|
|
55
58
|
function readFiles(db) {
|
|
56
59
|
const known = {};
|
|
@@ -129,10 +132,9 @@ function insertSql(file, r) {
|
|
|
129
132
|
`${nullNum(r.d)},${r.cr},${r.cw},${nullNum(r.usd)},${esc(r.st)},` +
|
|
130
133
|
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))});`;
|
|
131
134
|
}
|
|
132
|
-
// Incremental sync: unchanged transcripts are skipped via mtime+size,
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// (including nothing-to-do), false when sqlite3 or disk is unavailable.
|
|
135
|
+
// Incremental sync: unchanged transcripts are skipped via mtime+size, changed
|
|
136
|
+
// ones re-inserted. Rows for deleted transcripts are kept, so rotation never
|
|
137
|
+
// erases history. False when sqlite3 or disk is unavailable.
|
|
136
138
|
export function syncUsageDb() {
|
|
137
139
|
let db;
|
|
138
140
|
try {
|