@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
|
@@ -1,11 +1,10 @@
|
|
|
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.ts';
|
|
6
|
+
import { isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from '../lib/utils.ts';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
9
8
|
|
|
10
9
|
export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
|
|
11
10
|
|
|
@@ -21,11 +20,7 @@ export function ledgerPath(): string {
|
|
|
21
20
|
return tersioDataPath('usage.jsonl', 'tersio-usage.jsonl');
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
// Reset watermark:
|
|
25
|
-
// Session transcripts and the RTK database are host/tool-owned and never
|
|
26
|
-
// touched — instead, every derived view (token stats, command tools) filters
|
|
27
|
-
// rows from before the watermark, so "reset" empties what tersio shows
|
|
28
|
-
// without deleting anything it does not own.
|
|
23
|
+
// Reset watermark: views filter rows older than it, so nothing else is deleted.
|
|
29
24
|
export function resetMarkerPath(): string {
|
|
30
25
|
const override = process.env.TERSIO_RESET_FILE;
|
|
31
26
|
if (override) return override;
|
|
@@ -58,10 +53,7 @@ export function appendUsage(kind: UsageKind, detail: string): void {
|
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
// --- Session tokens, tokscale-style ------------------------------------------
|
|
61
|
-
//
|
|
62
|
-
// message.role === 'assistant', message.usage = { input, output, cacheRead,
|
|
63
|
-
// cacheWrite }, message.model, plus a per-line timestamp. Same source tokscale
|
|
64
|
-
// 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.
|
|
65
57
|
export interface TokenBreakdown {
|
|
66
58
|
input: number;
|
|
67
59
|
output: number;
|
|
@@ -69,10 +61,7 @@ export interface TokenBreakdown {
|
|
|
69
61
|
cacheWrite: number;
|
|
70
62
|
}
|
|
71
63
|
|
|
72
|
-
//
|
|
73
|
-
// (toolUse | stop | aborted | error) and, when the request failed, an HTTP
|
|
74
|
-
// errorStatus plus errorMessage. `toolUse` is an ordinary turn that handed off
|
|
75
|
-
// 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.
|
|
76
65
|
export type RunStatus = 'completed' | 'aborted' | 'error';
|
|
77
66
|
|
|
78
67
|
export interface RecentRequest {
|
|
@@ -83,9 +72,7 @@ export interface RecentRequest {
|
|
|
83
72
|
d?: number;
|
|
84
73
|
cr?: number;
|
|
85
74
|
cw?: number;
|
|
86
|
-
//
|
|
87
|
-
// Left undefined when the host recorded no cost — never backfilled with an
|
|
88
|
-
// estimate, so a measured figure is never mistaken for a modeled one.
|
|
75
|
+
// Undefined when the host recorded no cost; never backfilled.
|
|
89
76
|
usd?: number;
|
|
90
77
|
st: RunStatus;
|
|
91
78
|
code?: number;
|
|
@@ -104,6 +91,21 @@ export interface SessionTokens {
|
|
|
104
91
|
recent: RecentRequest[];
|
|
105
92
|
}
|
|
106
93
|
|
|
94
|
+
export interface RtkAdoption {
|
|
95
|
+
sessions: number;
|
|
96
|
+
bashCalls: number;
|
|
97
|
+
eligibleCalls: number;
|
|
98
|
+
rtkCalls: number;
|
|
99
|
+
missedCalls: number;
|
|
100
|
+
adoptionPct: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface RtkRecallDiagnostics {
|
|
104
|
+
mode: 'sqlite' | 'tee' | 'disabled' | 'unknown';
|
|
105
|
+
entries: number;
|
|
106
|
+
available: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
107
109
|
function zeroBreakdown(): TokenBreakdown {
|
|
108
110
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
109
111
|
}
|
|
@@ -111,10 +113,8 @@ export function durOf(v: unknown): number | undefined {
|
|
|
111
113
|
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
// Measured cost
|
|
115
|
-
//
|
|
116
|
-
// older fixtures. Anything else means "not recorded" — deliberately not 0, so
|
|
117
|
-
// 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.
|
|
118
118
|
export function costOf(usage: Record<string, unknown>): number | undefined {
|
|
119
119
|
const c = usage.cost;
|
|
120
120
|
if (typeof c === 'number') return Number.isFinite(c) ? c : undefined;
|
|
@@ -150,7 +150,11 @@ function addInto(into: TokenBreakdown, u: { input?: unknown; output?: unknown; c
|
|
|
150
150
|
export function sessionsDir(): string {
|
|
151
151
|
const override = process.env.TERSIO_SESSIONS_DIR;
|
|
152
152
|
if (override) return override;
|
|
153
|
-
|
|
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];
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
export function codexSessionsDir(): string {
|
|
@@ -182,13 +186,17 @@ export function walkJsonl(dir: string, out: string[], cap: number): void {
|
|
|
182
186
|
else if (e.isFile() && e.name.endsWith('.jsonl')) out.push(full);
|
|
183
187
|
}
|
|
184
188
|
}
|
|
185
|
-
// Recent requests
|
|
186
|
-
//
|
|
187
|
-
// (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.
|
|
188
191
|
export const RECENT_LIMIT = 200;
|
|
189
192
|
const FREE_SUFFIX = /(?::free|-free)$/i;
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
const RTK_ELIGIBLE_HEADS = new Set([
|
|
194
|
+
'rtk', 'git', 'grep', 'rg', 'find', 'cat', 'ls', 'tree', 'diff', 'log',
|
|
195
|
+
'npm', 'npx', 'pnpm', 'bun', 'bunx', 'cargo', 'go', 'python', 'pytest',
|
|
196
|
+
'ruff', 'mypy', 'docker', 'kubectl', 'psql', 'aws', 'gh', 'glab', 'wc',
|
|
197
|
+
]);
|
|
198
|
+
// Parse buffer shared by live reads and usage.db syncs, so the store is a
|
|
199
|
+
// cache and never a fork.
|
|
192
200
|
export interface SessionAccum {
|
|
193
201
|
byModel: Record<string, TokenBreakdown>;
|
|
194
202
|
byDay: Record<string, TokenBreakdown>;
|
|
@@ -253,19 +261,22 @@ export function classifySessionLine(row: { timestamp?: string | number; type?: u
|
|
|
253
261
|
const computedDur = (typeof msg.completedAt === 'number' && typeof msg.timestamp === 'number') ? msg.completedAt - msg.timestamp : undefined;
|
|
254
262
|
const tools: string[] = [];
|
|
255
263
|
for (const part of msg.content ?? []) {
|
|
256
|
-
if (part?.type
|
|
257
|
-
|
|
264
|
+
if (part?.type !== 'toolCall' || typeof part.name !== 'string' || !part.name) continue;
|
|
265
|
+
if (part.name !== 'bash') {
|
|
266
|
+
tools.push(part.name);
|
|
267
|
+
continue;
|
|
258
268
|
}
|
|
269
|
+
const command = (part.arguments as { command?: unknown } | null)?.command;
|
|
270
|
+
tools.push(`bash:${leadBinary(command)}`);
|
|
259
271
|
}
|
|
260
272
|
return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
|
|
261
273
|
}
|
|
262
|
-
// Fold `:free`/`-free` suffixes and case variants into one chart key
|
|
263
|
-
// same model from two providers stops splitting into separate rows.
|
|
274
|
+
// Fold `:free`/`-free` suffixes and case variants into one chart key.
|
|
264
275
|
export function canonicalModelId(model: string): string {
|
|
265
|
-
return model.replace(FREE_SUFFIX, '')
|
|
276
|
+
return canonicalPriceId(model.replace(FREE_SUFFIX, ''));
|
|
266
277
|
}
|
|
267
|
-
// LiteLLM-style display: namespace
|
|
268
|
-
//
|
|
278
|
+
// LiteLLM-style display: lowercase namespace, title-cased model segments with
|
|
279
|
+
// version dots kept.
|
|
269
280
|
export function displayModelId(model: string): string {
|
|
270
281
|
const bare = model.replace(FREE_SUFFIX, '');
|
|
271
282
|
const cap = (s: string): string => {
|
|
@@ -287,8 +298,8 @@ export function importSessionTokens(): SessionTokens {
|
|
|
287
298
|
const accum = newSessionAccum();
|
|
288
299
|
const files: string[] = [];
|
|
289
300
|
walkJsonl(sessionsDir(), files, 2000);
|
|
290
|
-
//
|
|
291
|
-
//
|
|
301
|
+
// An override means an isolated environment (tests, fixtures): only walk the
|
|
302
|
+
// real codex dir when one is explicitly set.
|
|
292
303
|
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
|
|
293
304
|
walkJsonl(codexSessionsDir(), files, 2000);
|
|
294
305
|
}
|
|
@@ -305,6 +316,80 @@ export function importSessionTokens(): SessionTokens {
|
|
|
305
316
|
accum.recent.sort((a, b) => b.t - a.t);
|
|
306
317
|
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) };
|
|
307
318
|
}
|
|
319
|
+
|
|
320
|
+
const adoptionFileCache = new Map<string, { size: number; mtimeMs: number; counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number } }>();
|
|
321
|
+
|
|
322
|
+
function parseAdoptionFile(file: string): { counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number }; sessions: number } {
|
|
323
|
+
const counts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
|
|
324
|
+
let text: string;
|
|
325
|
+
try {
|
|
326
|
+
text = fs.readFileSync(file, 'utf8');
|
|
327
|
+
} catch {
|
|
328
|
+
return { counts, sessions: 0 };
|
|
329
|
+
}
|
|
330
|
+
let sawBash = false;
|
|
331
|
+
for (const line of text.split('\n')) {
|
|
332
|
+
if (!line.includes('tool_execution_start')) continue;
|
|
333
|
+
try {
|
|
334
|
+
const row = JSON.parse(line) as {
|
|
335
|
+
type?: string;
|
|
336
|
+
customType?: string;
|
|
337
|
+
data?: { toolName?: unknown; args?: { command?: unknown } };
|
|
338
|
+
};
|
|
339
|
+
if (row.type !== 'custom' || row.customType !== 'tool_execution_start' || row.data?.toolName !== 'bash') continue;
|
|
340
|
+
const command = row.data.args?.command;
|
|
341
|
+
if (typeof command !== 'string') continue;
|
|
342
|
+
sawBash = true;
|
|
343
|
+
counts.bashCalls += 1;
|
|
344
|
+
const head = leadBinary(command);
|
|
345
|
+
if (RTK_ELIGIBLE_HEADS.has(head)) counts.eligibleCalls += 1;
|
|
346
|
+
if (head === 'rtk') counts.rtkCalls += 1;
|
|
347
|
+
} catch { /* skip corrupt line */ }
|
|
348
|
+
}
|
|
349
|
+
return { counts, sessions: sawBash ? 1 : 0 };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function clearRtkAdoptionCache(): void {
|
|
353
|
+
adoptionFileCache.clear();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function readRtkAdoption(): RtkAdoption {
|
|
357
|
+
const files: string[] = [];
|
|
358
|
+
walkJsonl(sessionsDir(), files, 2000);
|
|
359
|
+
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
|
|
360
|
+
walkJsonl(codexSessionsDir(), files, 2000);
|
|
361
|
+
}
|
|
362
|
+
const live = new Set(files);
|
|
363
|
+
for (const file of adoptionFileCache.keys()) {
|
|
364
|
+
if (!live.has(file)) adoptionFileCache.delete(file);
|
|
365
|
+
}
|
|
366
|
+
let bashCalls = 0;
|
|
367
|
+
let eligibleCalls = 0;
|
|
368
|
+
let rtkCalls = 0;
|
|
369
|
+
let sessions = 0;
|
|
370
|
+
for (const file of files) {
|
|
371
|
+
let stat: fs.Stats;
|
|
372
|
+
try {
|
|
373
|
+
stat = fs.statSync(file);
|
|
374
|
+
} catch {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
let entry = adoptionFileCache.get(file);
|
|
378
|
+
if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
|
|
379
|
+
const parsed = parseAdoptionFile(file);
|
|
380
|
+
entry = { size: stat.size, mtimeMs: stat.mtimeMs, counts: parsed.counts };
|
|
381
|
+
adoptionFileCache.set(file, entry);
|
|
382
|
+
sessions += parsed.sessions;
|
|
383
|
+
} else if (entry.counts.bashCalls > 0) {
|
|
384
|
+
sessions += 1;
|
|
385
|
+
}
|
|
386
|
+
bashCalls += entry.counts.bashCalls;
|
|
387
|
+
eligibleCalls += entry.counts.eligibleCalls;
|
|
388
|
+
rtkCalls += entry.counts.rtkCalls;
|
|
389
|
+
}
|
|
390
|
+
const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
|
|
391
|
+
return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
|
|
392
|
+
}
|
|
308
393
|
export function processSessionText(accum: SessionAccum, state: { codexProvider: string | null }, text: string): void {
|
|
309
394
|
for (const line of text.split('\n')) {
|
|
310
395
|
if (!line.trim()) continue;
|
|
@@ -325,8 +410,27 @@ export function processSessionText(accum: SessionAccum, state: { codexProvider:
|
|
|
325
410
|
} catch { /* skip corrupt lines */ }
|
|
326
411
|
}
|
|
327
412
|
}
|
|
328
|
-
|
|
329
|
-
|
|
413
|
+
|
|
414
|
+
export function readRtkRecallDiagnostics(binary: string | null = resolveRtkBinary()): RtkRecallDiagnostics {
|
|
415
|
+
if (!binary) return { mode: 'unknown', entries: 0, available: false };
|
|
416
|
+
try {
|
|
417
|
+
const configPath = process.env.RTK_CONFIG
|
|
418
|
+
|| (process.platform === 'darwin'
|
|
419
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'config.toml')
|
|
420
|
+
: path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'rtk', 'config.toml'));
|
|
421
|
+
let config = '';
|
|
422
|
+
try { config = fs.readFileSync(configPath, 'utf8'); } catch { /* use RTK CLI fallback */ }
|
|
423
|
+
const modeMatch = /^\s*mode\s*=\s*"(sqlite|tee|disabled)"/m.exec(config);
|
|
424
|
+
const mode = (modeMatch?.[1] ?? 'unknown') as RtkRecallDiagnostics['mode'];
|
|
425
|
+
const output = execFileSync(binary, ['recall', '--list'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
426
|
+
const entries = /\((\d+)\s+(?:entries?|stored)\)/i.exec(output)?.[1] ?? /^\s*(\d+)\s+entries?/im.exec(output)?.[1] ?? '0';
|
|
427
|
+
return { mode, entries: Number(entries) || 0, available: true };
|
|
428
|
+
} catch {
|
|
429
|
+
return { mode: 'unknown', entries: 0, available: false };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Lead binary of a shell string: first segment past `cd` chains and VAR=x
|
|
433
|
+
// assignments. Falls back to `bash`.
|
|
330
434
|
const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
|
|
331
435
|
export function leadBinary(command: unknown): string {
|
|
332
436
|
if (typeof command !== 'string' || !command.trim()) return 'bash';
|
|
@@ -341,7 +445,7 @@ export function leadBinary(command: unknown): string {
|
|
|
341
445
|
}
|
|
342
446
|
|
|
343
447
|
// --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
|
|
344
|
-
import { DEFAULT_PRICE, priceFor, refreshPricesIfStale } from './pricing.ts';
|
|
448
|
+
import { DEFAULT_PRICE, canonicalPriceId, priceFor, refreshPricesIfStale } from './pricing.ts';
|
|
345
449
|
import { co2GramsFor, energyWhFor } from './carbon.ts';
|
|
346
450
|
import type { ModelPrice } from './pricing.ts';
|
|
347
451
|
|
|
@@ -365,12 +469,6 @@ export function usdCost(t: TokenBreakdown, model?: string): { usd: number; price
|
|
|
365
469
|
};
|
|
366
470
|
}
|
|
367
471
|
|
|
368
|
-
// CO2 now model-differentiated via ./carbon.ts (EcoLogits 0.8.2 port).
|
|
369
|
-
// Always render with ~est. and never merge with measured figures.
|
|
370
|
-
// The constant below is the served gCO2eq per 1K output tokens for the
|
|
371
|
-
// default (gpt-4o-class) model, kept so single-figure callers stay honest.
|
|
372
|
-
export const CO2_G_PER_1K_OUTPUT = 0.2;
|
|
373
|
-
|
|
374
472
|
export function co2Grams(outputTokens: number, model?: string): number {
|
|
375
473
|
return co2GramsFor(model ?? 'gpt-4o', outputTokens);
|
|
376
474
|
}
|
|
@@ -395,9 +493,8 @@ export function readUsage(): UsageRow[] {
|
|
|
395
493
|
return rows;
|
|
396
494
|
}
|
|
397
495
|
|
|
398
|
-
// Delete the ledger file
|
|
399
|
-
//
|
|
400
|
-
// 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.
|
|
401
498
|
export function clearUsageLedger(): number {
|
|
402
499
|
const rows = readUsage().length;
|
|
403
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 {
|
|
@@ -1,11 +1,9 @@
|
|
|
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 os from 'node:os';
|
|
@@ -23,6 +21,7 @@ import {
|
|
|
23
21
|
walkJsonl,
|
|
24
22
|
} from './usage-ledger.ts';
|
|
25
23
|
import { tersioDataPath } from '../lib/utils.ts';
|
|
24
|
+
import { MODEL_ALIASES } from './pricing.ts';
|
|
26
25
|
import type { RunStatus, SessionTokens } from './usage-ledger.ts';
|
|
27
26
|
|
|
28
27
|
export function usageDbPath(): string {
|
|
@@ -70,6 +69,10 @@ function query(db: string, sql: string): string[][] {
|
|
|
70
69
|
|
|
71
70
|
function ensureSchema(db: string): void {
|
|
72
71
|
fs.mkdirSync(path.dirname(db), { recursive: true });
|
|
72
|
+
// Rows imported before an alias existed keep the spelling they were stored
|
|
73
|
+
// with, and the mtime ledger will not re-read those transcripts. Fold them in
|
|
74
|
+
// once, so the merge shows up without a reset.
|
|
75
|
+
const rekeys = MODEL_ALIASES.map((a) => `UPDATE messages SET model='${a.id}' WHERE model='${a.feed}';`).join('');
|
|
73
76
|
run(
|
|
74
77
|
db,
|
|
75
78
|
`CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);` +
|
|
@@ -77,7 +80,7 @@ function ensureSchema(db: string): void {
|
|
|
77
80
|
`CREATE TABLE IF NOT EXISTS messages (file TEXT NOT NULL, t REAL, model TEXT NOT NULL,` +
|
|
78
81
|
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
79
82
|
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]');` +
|
|
80
|
-
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file)
|
|
83
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file);` + rekeys,
|
|
81
84
|
);
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -91,8 +94,8 @@ function readFiles(db: string): Record<string, { mtime: number; size: number }>
|
|
|
91
94
|
return known;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
// One parsed assistant message, as stored. `t` is
|
|
95
|
-
//
|
|
97
|
+
// One parsed assistant message, as stored. `t` is null when the source row has
|
|
98
|
+
// no usable timestamp: counted, but skipped in the recent list.
|
|
96
99
|
interface StoredRow {
|
|
97
100
|
t: number | null;
|
|
98
101
|
model: string;
|
|
@@ -173,10 +176,9 @@ function insertSql(file: string, r: StoredRow): string {
|
|
|
173
176
|
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))});`;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
// Incremental sync: unchanged transcripts are skipped via mtime+size,
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// (including nothing-to-do), false when sqlite3 or disk is unavailable.
|
|
179
|
+
// Incremental sync: unchanged transcripts are skipped via mtime+size, changed
|
|
180
|
+
// ones re-inserted. Rows for deleted transcripts are kept, so rotation never
|
|
181
|
+
// erases history. False when sqlite3 or disk is unavailable.
|
|
180
182
|
export function syncUsageDb(): boolean {
|
|
181
183
|
let db: string;
|
|
182
184
|
try {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// publishes sync sibling mirrors live, so switches notify and take effect
|
|
5
5
|
// next turn — no reload.
|
|
6
6
|
import { getSharedComboState, reconcileSharedComboEntries, sessionEntries, } from '../shared/session-state.js';
|
|
7
|
+
import { setExtensionLabel } from '../shared/host.js';
|
|
7
8
|
import os from 'node:os';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { appendUsage, readUsage } from '../shared/usage-ledger.js';
|
|
@@ -48,7 +49,7 @@ async function openDashboard(pi, ctx) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
export default function tersioCommandsExtension(pi) {
|
|
51
|
-
pi
|
|
52
|
+
setExtensionLabel(pi, 'Tersio unified root command');
|
|
52
53
|
pi.registerCommand?.('tersio', {
|
|
53
54
|
description: 'Tersio root: status|check|update|dashboard|usage|help',
|
|
54
55
|
handler: async (args, ctx) => {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
reconcileSharedComboEntries,
|
|
9
9
|
sessionEntries,
|
|
10
10
|
} from '../shared/session-state.ts';
|
|
11
|
+
import { setExtensionLabel } from '../shared/host.ts';
|
|
11
12
|
import os from 'node:os';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
import { appendUsage, readUsage } from '../shared/usage-ledger.ts';
|
|
@@ -54,7 +55,7 @@ async function openDashboard(pi: ExtensionApi, ctx?: ExtensionCtx): Promise<void
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export default function tersioCommandsExtension(pi: ExtensionApi): void {
|
|
57
|
-
pi
|
|
58
|
+
setExtensionLabel(pi, 'Tersio unified root command');
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
pi.registerCommand?.('tersio', {
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.24.0",
|
|
4
|
+
"description": "\u2702\ufe0f Token-saving OMP add-ons: terse Caveman replies, compact RTK shell output, lean Ponytail code calls \u2014 one-command combo presets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"omp",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-package",
|
|
7
9
|
"token-saving",
|
|
8
10
|
"ai-tools",
|
|
9
11
|
"developer-tools",
|
|
@@ -29,7 +31,6 @@
|
|
|
29
31
|
"./extensions/caveman-session/index.ts",
|
|
30
32
|
"./extensions/rtk-session/index.ts",
|
|
31
33
|
"./extensions/combo-toggle/index.ts",
|
|
32
|
-
"./extensions/shared/mode-reinforcement.ts",
|
|
33
34
|
"./extensions/tersio-commands/index.ts"
|
|
34
35
|
],
|
|
35
36
|
"skills": [
|
|
@@ -56,6 +57,11 @@
|
|
|
56
57
|
],
|
|
57
58
|
"default": "off"
|
|
58
59
|
},
|
|
60
|
+
"comboSetupComplete": {
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"description": "Whether the first interactive OMP launch has completed the Combo preset prompt",
|
|
63
|
+
"default": false
|
|
64
|
+
},
|
|
59
65
|
"cavemanDefault": {
|
|
60
66
|
"type": "enum",
|
|
61
67
|
"description": "Session-start default (no saved state)",
|
|
@@ -64,7 +70,9 @@
|
|
|
64
70
|
"lite",
|
|
65
71
|
"full",
|
|
66
72
|
"ultra",
|
|
67
|
-
"wenyan"
|
|
73
|
+
"wenyan-lite",
|
|
74
|
+
"wenyan-full",
|
|
75
|
+
"wenyan-ultra"
|
|
68
76
|
],
|
|
69
77
|
"default": "off"
|
|
70
78
|
},
|
|
@@ -80,8 +88,7 @@
|
|
|
80
88
|
"off",
|
|
81
89
|
"lite",
|
|
82
90
|
"full",
|
|
83
|
-
"ultra"
|
|
84
|
-
"review"
|
|
91
|
+
"ultra"
|
|
85
92
|
],
|
|
86
93
|
"default": "off"
|
|
87
94
|
},
|
|
@@ -109,7 +116,8 @@
|
|
|
109
116
|
"cli/**/*.js",
|
|
110
117
|
"dashboard/dist/**/*",
|
|
111
118
|
"extensions/**/*.js",
|
|
112
|
-
"extensions/**/*.ts"
|
|
119
|
+
"extensions/**/*.ts",
|
|
120
|
+
"extensions/caveman-session/rule.md"
|
|
113
121
|
],
|
|
114
122
|
"scripts": {
|
|
115
123
|
"clean": "rm -f tersio.js cli/*.js extensions/**/*.js && rm -rf dashboard/dist",
|
package/tersio.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// tersio.ts — Install Tersio (caveman/rtk/ponytail) add-ons on any OMP device.
|
|
3
|
-
// Usage: node tersio.js [install|update|
|
|
3
|
+
// Usage: node tersio.js [install|update|doctor|dashboard|uninstall|version|help] [options]
|
|
4
4
|
// Requires: node/npm and omp CLI
|
|
5
5
|
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, dashboardExport, dashboardOpen, dashboardPort, doctor, reset, settings, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
|
|
6
6
|
import { closeRL } from './cli/interactive.js';
|
|
@@ -16,9 +16,8 @@ function printHelp() {
|
|
|
16
16
|
console.log(`Usage: ${PACKAGE_BIN} [command] [options]
|
|
17
17
|
|
|
18
18
|
Commands:
|
|
19
|
-
install Install the add-ons (
|
|
19
|
+
install Install the add-ons for one agent (--host omp|pi, or ask at a terminal)
|
|
20
20
|
update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail)
|
|
21
|
-
reinstall Clean and reinstall the user-scope add-ons
|
|
22
21
|
doctor Check the current installation (--fix repairs, --dry-run previews)
|
|
23
22
|
usage Ledger-backed usage + savings report
|
|
24
23
|
dashboard Open the Dashboard (localhost only)
|
|
@@ -31,11 +30,12 @@ Commands:
|
|
|
31
30
|
Options:
|
|
32
31
|
--fix (doctor: repairs all; --fix=<scope> repairs one of extensions, registrations, rtk, ponytail, cli)
|
|
33
32
|
--scope user (legacy; accepted and ignored — user scope is the only scope)
|
|
33
|
+
--host omp|pi (install|uninstall: target agent; a terminal asks instead)
|
|
34
34
|
--keep-ponytail (uninstall: keep the bundled Ponytail copy — removed by default)
|
|
35
|
-
--remove-rtk (uninstall: also remove the RTK binary)
|
|
35
|
+
--remove-rtk (uninstall: also remove the RTK binary; its OMP wiring always goes)
|
|
36
36
|
--combo-default off|medium|balanced|max (implies caveman, rtk, ponytail)
|
|
37
|
-
--caveman-default off|lite|full|ultra|wenyan (override; default follows combo)
|
|
38
|
-
--ponytail-default off|lite|full|ultra
|
|
37
|
+
--caveman-default off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra (override; default follows combo)
|
|
38
|
+
--ponytail-default off|lite|full|ultra (override; default follows combo)
|
|
39
39
|
--rtk-default on|off (override; default follows combo)
|
|
40
40
|
--yes, -y
|
|
41
41
|
--dry-run
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { asPromptArray, reconcileSharedComboEntries, sessionEntries, systemPromptIncludes } from './session-state.js';
|
|
2
|
-
const MARKER = 'SUPREME TOKEN SAVER MODES ACTIVE';
|
|
3
|
-
function instruction(state) {
|
|
4
|
-
const modes = [
|
|
5
|
-
state.caveman !== 'off' && `caveman=${state.caveman}`,
|
|
6
|
-
state.rtk === 'on' && 'rtk=on',
|
|
7
|
-
state.ponytail !== 'off' && `ponytail=${state.ponytail}`,
|
|
8
|
-
].filter(Boolean);
|
|
9
|
-
if (!modes.length)
|
|
10
|
-
return null;
|
|
11
|
-
return `${MARKER}: ${modes.join(' · ')}. Keep these active for the entire response: concise Caveman prose, RTK for eligible noisy shell output, and the smallest correct Ponytail solution. Do not weaken or disable a mode unless the user explicitly asks.`;
|
|
12
|
-
}
|
|
13
|
-
export default function modeReinforcementExtension(pi) {
|
|
14
|
-
pi.setLabel?.('Tersio mode reinforcement');
|
|
15
|
-
pi.on('before_agent_start', async (event, ctx) => {
|
|
16
|
-
if (!ctx?.hasUI)
|
|
17
|
-
return;
|
|
18
|
-
const text = instruction(reconcileSharedComboEntries(sessionEntries(ctx)));
|
|
19
|
-
if (!text)
|
|
20
|
-
return;
|
|
21
|
-
const base = asPromptArray(event.systemPrompt);
|
|
22
|
-
if (systemPromptIncludes(base, MARKER))
|
|
23
|
-
return;
|
|
24
|
-
return { systemPrompt: [...base, text] };
|
|
25
|
-
});
|
|
26
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { asPromptArray, reconcileSharedComboEntries, sessionEntries, systemPromptIncludes } from './session-state.ts';
|
|
2
|
-
import type { ExtensionApi, SystemPromptEvent } from './types.ts';
|
|
3
|
-
|
|
4
|
-
const MARKER = 'SUPREME TOKEN SAVER MODES ACTIVE';
|
|
5
|
-
|
|
6
|
-
function instruction(state: { caveman: string; rtk: string; ponytail: string }): string | null {
|
|
7
|
-
const modes = [
|
|
8
|
-
state.caveman !== 'off' && `caveman=${state.caveman}`,
|
|
9
|
-
state.rtk === 'on' && 'rtk=on',
|
|
10
|
-
state.ponytail !== 'off' && `ponytail=${state.ponytail}`,
|
|
11
|
-
].filter(Boolean);
|
|
12
|
-
if (!modes.length) return null;
|
|
13
|
-
|
|
14
|
-
return `${MARKER}: ${modes.join(' · ')}. Keep these active for the entire response: concise Caveman prose, RTK for eligible noisy shell output, and the smallest correct Ponytail solution. Do not weaken or disable a mode unless the user explicitly asks.`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export default function modeReinforcementExtension(pi: ExtensionApi): void {
|
|
18
|
-
pi.setLabel?.('Tersio mode reinforcement');
|
|
19
|
-
|
|
20
|
-
pi.on('before_agent_start', async (event: SystemPromptEvent, ctx) => {
|
|
21
|
-
if (!ctx?.hasUI) return;
|
|
22
|
-
const text = instruction(reconcileSharedComboEntries(sessionEntries(ctx)));
|
|
23
|
-
if (!text) return;
|
|
24
|
-
|
|
25
|
-
const base = asPromptArray(event.systemPrompt);
|
|
26
|
-
if (systemPromptIncludes(base, MARKER)) return;
|
|
27
|
-
return { systemPrompt: [...base, text] };
|
|
28
|
-
});
|
|
29
|
-
}
|