@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.
- package/README.md +62 -46
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +55 -55
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +76 -49
- package/cli/doctor-fix.js +61 -44
- package/cli/doctor.js +66 -44
- package/cli/hosts.js +118 -0
- package/cli/install.js +187 -143
- 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 +45 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/settings.js +1 -1
- package/cli/uninstall.js +135 -44
- package/cli/update.js +24 -30
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +36 -25
- package/extensions/ai-addons-updater/index.ts +36 -24
- package/extensions/caveman-session/index.js +23 -18
- package/extensions/caveman-session/index.ts +23 -18
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +63 -41
- package/extensions/combo-toggle/index.ts +61 -48
- package/extensions/lib/utils.js +56 -13
- package/extensions/lib/utils.ts +55 -13
- package/extensions/rtk-session/index.js +20 -19
- package/extensions/rtk-session/index.ts +18 -24
- 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 +63 -16
- package/extensions/shared/plugin-settings.ts +67 -17
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +13 -24
- package/extensions/shared/rtk-gain.ts +14 -25
- package/extensions/shared/session-state.js +26 -28
- package/extensions/shared/session-state.ts +26 -29
- package/extensions/shared/types.ts +15 -3
- package/extensions/shared/usage-ledger.js +136 -38
- package/extensions/shared/usage-ledger.ts +149 -52
- 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 +15 -7
- package/tersio.js +6 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
- 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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
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
|
-
//
|
|
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
|
|
181
|
-
//
|
|
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
|
|
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
|
|
193
|
-
//
|
|
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
|
|
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 { 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:
|
|
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
|
|
58
|
-
//
|
|
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
|
-
|
|
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
|
|
136
|
-
//
|
|
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
|
|
198
|
-
|
|
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
|
|
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, '')
|
|
211
|
+
return canonicalPriceId(model.replace(FREE_SUFFIX, ''));
|
|
207
212
|
}
|
|
208
|
-
// LiteLLM-style display: namespace
|
|
209
|
-
//
|
|
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
|
-
//
|
|
236
|
-
//
|
|
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
|
-
|
|
279
|
-
|
|
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
|
|
346
|
-
//
|
|
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 {
|