@krtclcdy/tersio 2.23.0 → 2.24.1
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 +56 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/banner.js +2 -3
- package/cli/common.js +16 -20
- package/cli/currency.js +11 -19
- package/cli/dashboard.js +216 -40
- package/cli/doctor-fix.js +40 -26
- package/cli/doctor.js +41 -41
- package/cli/hosts.js +114 -0
- package/cli/install.js +284 -179
- package/cli/interactive.js +62 -2
- package/cli/layer.js +22 -0
- package/cli/manifest.js +33 -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 +48 -48
- package/cli/reset.js +5 -32
- package/cli/rtk-wiring.js +7 -17
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +162 -52
- package/cli/update.js +19 -27
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +37 -29
- package/extensions/ai-addons-updater/index.ts +39 -38
- package/extensions/caveman-session/index.js +19 -34
- package/extensions/caveman-session/index.ts +20 -35
- package/extensions/combo-toggle/index.js +47 -49
- package/extensions/combo-toggle/index.ts +42 -58
- package/extensions/lib/utils.js +40 -9
- package/extensions/lib/utils.ts +40 -9
- package/extensions/rtk-session/index.js +34 -38
- package/extensions/rtk-session/index.ts +32 -41
- package/extensions/shared/carbon.js +3 -11
- package/extensions/shared/carbon.ts +4 -11
- package/extensions/shared/host.js +58 -0
- package/extensions/shared/host.ts +94 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +33 -30
- package/extensions/shared/plugin-settings.ts +35 -26
- package/extensions/shared/pricing.js +25 -16
- package/extensions/shared/pricing.ts +27 -17
- package/extensions/shared/rtk-gain.js +6 -22
- package/extensions/shared/rtk-gain.ts +6 -22
- package/extensions/shared/session-state.js +44 -36
- package/extensions/shared/session-state.ts +48 -35
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +160 -51
- package/extensions/shared/usage-ledger.ts +170 -67
- package/extensions/shared/usage-store.js +178 -24
- package/extensions/shared/usage-store.ts +176 -25
- package/extensions/tersio-commands/index.js +3 -6
- package/extensions/tersio-commands/index.ts +3 -6
- package/package.json +6 -5
- package/tersio.js +4 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Reads omp's persisted plugin state (~/.omp/plugins/omp-plugins.lock.json),
|
|
3
|
-
// written by `omp plugin config set @krtclcdy/tersio <key> <value>` and by
|
|
4
|
-
// the installer profile step. Tolerant: any parse failure yields {} so every
|
|
5
|
-
// caller falls back to its own default.
|
|
1
|
+
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
6
2
|
|
|
7
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
4
|
import os from 'node:os';
|
|
9
5
|
import path from 'node:path';
|
|
6
|
+
import { tersioHome } from '../lib/utils.ts';
|
|
7
|
+
|
|
10
8
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
// The one store both hosts read; this module owns its path.
|
|
11
|
+
export function tersioSettingsFile(): string {
|
|
12
|
+
return path.join(tersioHome(), 'settings.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Older installs kept these as OMP plugin settings; still read so switching
|
|
16
|
+
// the store does not reset a user's defaults.
|
|
17
|
+
function legacyOmpSettings(): Record<string, unknown> | null {
|
|
13
18
|
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
14
|
-
|
|
19
|
+
for (const p of [
|
|
15
20
|
path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
16
21
|
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
17
|
-
]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// `settings[PLUGIN_NAME]` in the lock file; empty object when missing.
|
|
21
|
-
export function readPluginSettings(): Record<string, unknown> {
|
|
22
|
-
for (const p of lockPaths()) {
|
|
22
|
+
]) {
|
|
23
23
|
if (!existsSync(p)) continue;
|
|
24
24
|
try {
|
|
25
25
|
const config = JSON.parse(readFileSync(p, 'utf8')) as { settings?: Record<string, Record<string, unknown>> };
|
|
@@ -27,7 +27,23 @@ export function readPluginSettings(): Record<string, unknown> {
|
|
|
27
27
|
if (values && typeof values === 'object' && !Array.isArray(values)) return values;
|
|
28
28
|
} catch { /* tolerate corrupt file */ }
|
|
29
29
|
}
|
|
30
|
-
return
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readSettingsFile(): Record<string, unknown> | null {
|
|
34
|
+
const p = tersioSettingsFile();
|
|
35
|
+
if (!existsSync(p)) return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(readFileSync(p, 'utf8')) as Record<string, unknown>;
|
|
38
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The stored settings; empty when the file is missing, corrupt, or empty.
|
|
45
|
+
export function readPluginSettings(): Record<string, unknown> {
|
|
46
|
+
return readSettingsFile() ?? legacyOmpSettings() ?? {};
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
|
|
@@ -69,20 +85,13 @@ const COMBO_MODE_DEFAULTS = {
|
|
|
69
85
|
|
|
70
86
|
export function saveComboSetup(level: string): boolean {
|
|
71
87
|
if (!(level in COMBO_MODE_DEFAULTS)) return false;
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
lock.plugins ||= {};
|
|
78
|
-
lock.settings ||= {};
|
|
79
|
-
lock.settings[PLUGIN_NAME] = {
|
|
80
|
-
...lock.settings[PLUGIN_NAME],
|
|
88
|
+
const file = tersioSettingsFile();
|
|
89
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
90
|
+
writeFileSync(file, `${JSON.stringify({
|
|
91
|
+
...readPluginSettings(),
|
|
81
92
|
comboDefault: level,
|
|
82
93
|
...COMBO_MODE_DEFAULTS[level as keyof typeof COMBO_MODE_DEFAULTS],
|
|
83
94
|
comboSetupComplete: true,
|
|
84
|
-
};
|
|
85
|
-
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
86
|
-
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
95
|
+
}, null, 2)}\n`, 'utf8');
|
|
87
96
|
return true;
|
|
88
97
|
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM.
|
|
2
|
-
//
|
|
3
|
-
// The cache is the full LiteLLM feed (3k+ ids), refreshed lazily in background
|
|
4
|
-
// and on `tersio update`; every reader is sync and offline-safe. No static
|
|
5
|
-
// per-model table — list prices rot, the feed does not. Unknown models report
|
|
6
|
-
// the default with known:false so the dashboard labels them honestly.
|
|
1
|
+
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM. Exact id
|
|
2
|
+
// → provider-prefix strip → default, reported with known:false when unpriced.
|
|
7
3
|
import fs from 'node:fs';
|
|
8
4
|
import path from 'node:path';
|
|
9
5
|
import { tersioDataPath } from '../lib/utils.js';
|
|
10
6
|
// Sonnet-class default for unpriced models; always paired with known:false.
|
|
11
7
|
export const DEFAULT_PRICE = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
|
|
12
8
|
const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
13
|
-
//
|
|
14
|
-
//
|
|
9
|
+
// Readers use the file even when stale, and only one refresh runs per process;
|
|
10
|
+
// `tersio update` still forces a fresh fetch.
|
|
15
11
|
const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
16
12
|
let refreshInflight = null;
|
|
17
13
|
export function pricesCachePath() {
|
|
@@ -26,8 +22,8 @@ export function pricesUrl() {
|
|
|
26
22
|
function toPrice(t) {
|
|
27
23
|
return { input: t[0], output: t[1], cacheRead: t[2], cacheWrite: t[3] };
|
|
28
24
|
}
|
|
29
|
-
// Tolerate both cache shapes
|
|
30
|
-
//
|
|
25
|
+
// Tolerate both cache shapes (compact tuples and ModelPrice objects) so a
|
|
26
|
+
// version bump never orphans the cache.
|
|
31
27
|
function asPrice(v) {
|
|
32
28
|
if (Array.isArray(v) && v.length === 4 && v.every((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)) {
|
|
33
29
|
return toPrice(v);
|
|
@@ -35,6 +31,8 @@ function asPrice(v) {
|
|
|
35
31
|
if (v && typeof v === 'object') {
|
|
36
32
|
const o = v;
|
|
37
33
|
if (['input', 'output', 'cacheRead', 'cacheWrite'].every((k) => typeof o[k] === 'number' && Number.isFinite(o[k]) && o[k] >= 0)) {
|
|
34
|
+
// SAFETY: the line above verified all four keys are finite numbers >= 0,
|
|
35
|
+
// which is the whole of ModelPrice.
|
|
38
36
|
return o;
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -71,15 +69,27 @@ export function loadLivePrices() {
|
|
|
71
69
|
function num(v) {
|
|
72
70
|
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
73
71
|
}
|
|
72
|
+
// Transcripts and the feed spell one model differently; aliases stop a free
|
|
73
|
+
// model reporting the default price.
|
|
74
|
+
export const MODEL_ALIASES = [
|
|
75
|
+
{ id: 'space-bunny', feed: 'stealth/space-bunny-alpha' },
|
|
76
|
+
];
|
|
77
|
+
export function canonicalPriceId(model) {
|
|
78
|
+
const key = model.toLowerCase();
|
|
79
|
+
return MODEL_ALIASES.find((a) => a.id === key || a.feed === key)?.id ?? key;
|
|
80
|
+
}
|
|
74
81
|
export function priceFor(model, live) {
|
|
75
82
|
const table = live === undefined ? loadLivePrices() : live;
|
|
83
|
+
// An alias goes through the same exact-then-suffix chain as any other id, so
|
|
84
|
+
// it still resolves when the feed keys it under a provider prefix.
|
|
85
|
+
const id = MODEL_ALIASES.find((a) => a.id === model.toLowerCase())?.feed ?? model;
|
|
76
86
|
if (table) {
|
|
77
|
-
const hit = asPrice(table.exact[
|
|
87
|
+
const hit = asPrice(table.exact[id] ?? table.exact[id.toLowerCase()]);
|
|
78
88
|
if (hit)
|
|
79
89
|
return { price: hit, known: true, live: true };
|
|
80
90
|
// Provider-prefixed ids ("azure/gpt-4o", "dashscope/qwen-max"): match the
|
|
81
91
|
// first cached id that ends with the bare model name.
|
|
82
|
-
const name =
|
|
92
|
+
const name = id.toLowerCase();
|
|
83
93
|
const key = Object.keys(table.exact).find((k) => k.toLowerCase() === name || k.toLowerCase().endsWith(`/${name}`));
|
|
84
94
|
const prefixed = key ? asPrice(table.exact[key]) : null;
|
|
85
95
|
if (prefixed)
|
|
@@ -87,8 +97,7 @@ export function priceFor(model, live) {
|
|
|
87
97
|
}
|
|
88
98
|
return { price: DEFAULT_PRICE, known: false, live: false };
|
|
89
99
|
}
|
|
90
|
-
// Fetch LiteLLM pricing
|
|
91
|
-
// the cache. False on any failure (offline, timeout, shape change) — callers
|
|
100
|
+
// Fetch LiteLLM pricing and write the cache. False on any failure — callers
|
|
92
101
|
// fall back silently.
|
|
93
102
|
export async function refreshPrices() {
|
|
94
103
|
if (refreshInflight)
|
|
@@ -153,8 +162,8 @@ export async function refreshPrices() {
|
|
|
153
162
|
refreshInflight = null;
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
|
-
// Fire-and-forget refresh when the cache is stale
|
|
157
|
-
//
|
|
165
|
+
// Fire-and-forget refresh when the cache is stale; readers keep the stale
|
|
166
|
+
// file, so pricing never blocks.
|
|
158
167
|
export function refreshPricesIfStale() {
|
|
159
168
|
const live = loadLivePrices();
|
|
160
169
|
if (live && Date.now() - live.fetchedAt <= CACHE_TTL_MS)
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM.
|
|
2
|
-
//
|
|
3
|
-
// The cache is the full LiteLLM feed (3k+ ids), refreshed lazily in background
|
|
4
|
-
// and on `tersio update`; every reader is sync and offline-safe. No static
|
|
5
|
-
// per-model table — list prices rot, the feed does not. Unknown models report
|
|
6
|
-
// the default with known:false so the dashboard labels them honestly.
|
|
1
|
+
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM. Exact id
|
|
2
|
+
// → provider-prefix strip → default, reported with known:false when unpriced.
|
|
7
3
|
import fs from 'node:fs';
|
|
8
|
-
import os from 'node:os';
|
|
9
4
|
import path from 'node:path';
|
|
10
5
|
import { tersioDataPath } from '../lib/utils.ts';
|
|
11
6
|
|
|
@@ -20,8 +15,8 @@ export interface ModelPrice {
|
|
|
20
15
|
export const DEFAULT_PRICE: ModelPrice = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
|
|
21
16
|
|
|
22
17
|
const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
23
|
-
//
|
|
24
|
-
//
|
|
18
|
+
// Readers use the file even when stale, and only one refresh runs per process;
|
|
19
|
+
// `tersio update` still forces a fresh fetch.
|
|
25
20
|
const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
26
21
|
let refreshInflight: Promise<boolean> | null = null;
|
|
27
22
|
|
|
@@ -47,8 +42,8 @@ function toPrice(t: [number, number, number, number]): ModelPrice {
|
|
|
47
42
|
return { input: t[0], output: t[1], cacheRead: t[2], cacheWrite: t[3] };
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
// Tolerate both cache shapes
|
|
51
|
-
//
|
|
45
|
+
// Tolerate both cache shapes (compact tuples and ModelPrice objects) so a
|
|
46
|
+
// version bump never orphans the cache.
|
|
52
47
|
function asPrice(v: unknown): ModelPrice | null {
|
|
53
48
|
if (Array.isArray(v) && v.length === 4 && v.every((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)) {
|
|
54
49
|
return toPrice(v as [number, number, number, number]);
|
|
@@ -56,6 +51,8 @@ function asPrice(v: unknown): ModelPrice | null {
|
|
|
56
51
|
if (v && typeof v === 'object') {
|
|
57
52
|
const o = v as Record<string, unknown>;
|
|
58
53
|
if (['input', 'output', 'cacheRead', 'cacheWrite'].every((k) => typeof o[k] === 'number' && Number.isFinite(o[k]) && (o[k] as number) >= 0)) {
|
|
54
|
+
// SAFETY: the line above verified all four keys are finite numbers >= 0,
|
|
55
|
+
// which is the whole of ModelPrice.
|
|
59
56
|
return o as unknown as ModelPrice;
|
|
60
57
|
}
|
|
61
58
|
}
|
|
@@ -90,22 +87,35 @@ function num(v: unknown): number | null {
|
|
|
90
87
|
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
91
88
|
}
|
|
92
89
|
|
|
90
|
+
// Transcripts and the feed spell one model differently; aliases stop a free
|
|
91
|
+
// model reporting the default price.
|
|
92
|
+
export const MODEL_ALIASES: ReadonlyArray<{ id: string; feed: string }> = [
|
|
93
|
+
{ id: 'space-bunny', feed: 'stealth/space-bunny-alpha' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
export function canonicalPriceId(model: string): string {
|
|
97
|
+
const key = model.toLowerCase();
|
|
98
|
+
return MODEL_ALIASES.find((a) => a.id === key || a.feed === key)?.id ?? key;
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
export function priceFor(model: string, live?: LivePrices | null): { price: ModelPrice; known: boolean; live: boolean } {
|
|
94
102
|
const table = live === undefined ? loadLivePrices() : live;
|
|
103
|
+
// An alias goes through the same exact-then-suffix chain as any other id, so
|
|
104
|
+
// it still resolves when the feed keys it under a provider prefix.
|
|
105
|
+
const id = MODEL_ALIASES.find((a) => a.id === model.toLowerCase())?.feed ?? model;
|
|
95
106
|
if (table) {
|
|
96
|
-
const hit = asPrice(table.exact[
|
|
107
|
+
const hit = asPrice(table.exact[id] ?? table.exact[id.toLowerCase()]);
|
|
97
108
|
if (hit) return { price: hit, known: true, live: true };
|
|
98
109
|
// Provider-prefixed ids ("azure/gpt-4o", "dashscope/qwen-max"): match the
|
|
99
110
|
// first cached id that ends with the bare model name.
|
|
100
|
-
const name =
|
|
111
|
+
const name = id.toLowerCase();
|
|
101
112
|
const key = Object.keys(table.exact).find((k) => k.toLowerCase() === name || k.toLowerCase().endsWith(`/${name}`));
|
|
102
113
|
const prefixed = key ? asPrice(table.exact[key]) : null;
|
|
103
114
|
if (prefixed) return { price: prefixed, known: true, live: true };
|
|
104
115
|
}
|
|
105
116
|
return { price: DEFAULT_PRICE, known: false, live: false };
|
|
106
117
|
}
|
|
107
|
-
// Fetch LiteLLM pricing
|
|
108
|
-
// the cache. False on any failure (offline, timeout, shape change) — callers
|
|
118
|
+
// Fetch LiteLLM pricing and write the cache. False on any failure — callers
|
|
109
119
|
// fall back silently.
|
|
110
120
|
export async function refreshPrices(): Promise<boolean> {
|
|
111
121
|
if (refreshInflight) return refreshInflight;
|
|
@@ -160,8 +170,8 @@ export async function refreshPrices(): Promise<boolean> {
|
|
|
160
170
|
}
|
|
161
171
|
}
|
|
162
172
|
|
|
163
|
-
// Fire-and-forget refresh when the cache is stale
|
|
164
|
-
//
|
|
173
|
+
// Fire-and-forget refresh when the cache is stale; readers keep the stale
|
|
174
|
+
// file, so pricing never blocks.
|
|
165
175
|
export function refreshPricesIfStale(): void {
|
|
166
176
|
const live = loadLivePrices();
|
|
167
177
|
if (live && Date.now() - live.fetchedAt <= CACHE_TTL_MS) return;
|
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
// RTK records every invocation (input/output/saved tokens, savings %, exec
|
|
3
|
-
// time) in history.db; `rtk gain` renders the same rows. We read them with
|
|
4
|
-
// the sqlite3 CLI (present on macOS; best-effort elsewhere) so the dashboard
|
|
5
|
-
// shows measured figures, never estimates. Missing DB/CLI → empty, no throw.
|
|
6
|
-
//
|
|
7
|
-
// Caveman and Ponytail have no per-command counters (instruction-following
|
|
8
|
-
// isn't metered); their gains are bench-measured in BENCHMARK.md and cited
|
|
9
|
-
// as static figures wherever RTK rows appear.
|
|
10
|
-
//
|
|
11
|
-
// Rows group by command alone, machine-wide: one row per command no matter
|
|
12
|
-
// which repo ran it. Project filtering stays out — wasted column, same logic.
|
|
1
|
+
// Savings read from rtk history, never estimated; modes bench in BENCHMARK.md.
|
|
13
2
|
import { execFileSync } from 'node:child_process';
|
|
14
3
|
import fs from 'node:fs';
|
|
15
4
|
import os from 'node:os';
|
|
16
5
|
import path from 'node:path';
|
|
17
6
|
const EMPTY = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0, byCommand: [] };
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// before it crosses the sqlite3 CLI and lands in every /data.json poll.
|
|
7
|
+
// The table shows one clipped cell, so bound the command before it crosses the
|
|
8
|
+
// sqlite3 CLI and lands in every /data.json poll.
|
|
21
9
|
const MAX_CMD_CHARS = 200;
|
|
22
|
-
//
|
|
23
|
-
// `node -e` scripts verbatim (~67 rows here), so one logical row can arrive as
|
|
24
|
-
// several lines and derail every row after it. Fold whitespace into spaces
|
|
25
|
-
// first: the stored command keeps its newlines, only the read view flattens.
|
|
10
|
+
// A multi-line stored command would derail every row after it; flatten on read.
|
|
26
11
|
const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13), ' '), char(9), ' ')`;
|
|
27
12
|
export function rtkDbPath() {
|
|
28
13
|
const override = process.env.TERSIO_RTK_DB;
|
|
@@ -49,9 +34,8 @@ export function readRtkGain(limit = 10, cutoffMs) {
|
|
|
49
34
|
try {
|
|
50
35
|
if (!fs.existsSync(rtkDbPath()))
|
|
51
36
|
return EMPTY;
|
|
52
|
-
// View-level cutoff only:
|
|
53
|
-
//
|
|
54
|
-
// with unparseable timestamps (0) drop out of a filtered view.
|
|
37
|
+
// View-level cutoff only: rtk's own rows stay untouched. Timestamps that do
|
|
38
|
+
// not parse land on 0 and drop out of a filtered view.
|
|
55
39
|
const where = cutoffMs && cutoffMs > 0
|
|
56
40
|
? `WHERE CAST(strftime('%s', timestamp) AS INTEGER) >= ${Math.floor(cutoffMs / 1000)}`
|
|
57
41
|
: '';
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// RTK records every invocation (input/output/saved tokens, savings %, exec
|
|
3
|
-
// time) in history.db; `rtk gain` renders the same rows. We read them with
|
|
4
|
-
// the sqlite3 CLI (present on macOS; best-effort elsewhere) so the dashboard
|
|
5
|
-
// shows measured figures, never estimates. Missing DB/CLI → empty, no throw.
|
|
6
|
-
//
|
|
7
|
-
// Caveman and Ponytail have no per-command counters (instruction-following
|
|
8
|
-
// isn't metered); their gains are bench-measured in BENCHMARK.md and cited
|
|
9
|
-
// as static figures wherever RTK rows appear.
|
|
10
|
-
//
|
|
11
|
-
// Rows group by command alone, machine-wide: one row per command no matter
|
|
12
|
-
// which repo ran it. Project filtering stays out — wasted column, same logic.
|
|
1
|
+
// Savings read from rtk history, never estimated; modes bench in BENCHMARK.md.
|
|
13
2
|
import { execFileSync } from 'node:child_process';
|
|
14
3
|
import fs from 'node:fs';
|
|
15
4
|
import os from 'node:os';
|
|
@@ -34,15 +23,11 @@ export interface RtkGain {
|
|
|
34
23
|
|
|
35
24
|
const EMPTY: RtkGain = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0, byCommand: [] };
|
|
36
25
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// before it crosses the sqlite3 CLI and lands in every /data.json poll.
|
|
26
|
+
// The table shows one clipped cell, so bound the command before it crosses the
|
|
27
|
+
// sqlite3 CLI and lands in every /data.json poll.
|
|
40
28
|
const MAX_CMD_CHARS = 200;
|
|
41
29
|
|
|
42
|
-
//
|
|
43
|
-
// `node -e` scripts verbatim (~67 rows here), so one logical row can arrive as
|
|
44
|
-
// several lines and derail every row after it. Fold whitespace into spaces
|
|
45
|
-
// first: the stored command keeps its newlines, only the read view flattens.
|
|
30
|
+
// A multi-line stored command would derail every row after it; flatten on read.
|
|
46
31
|
const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13), ' '), char(9), ' ')`;
|
|
47
32
|
|
|
48
33
|
export function rtkDbPath(): string {
|
|
@@ -70,9 +55,8 @@ function query(db: string, sql: string): string[][] {
|
|
|
70
55
|
export function readRtkGain(limit = 10, cutoffMs?: number): RtkGain {
|
|
71
56
|
try {
|
|
72
57
|
if (!fs.existsSync(rtkDbPath())) return EMPTY;
|
|
73
|
-
// View-level cutoff only:
|
|
74
|
-
//
|
|
75
|
-
// with unparseable timestamps (0) drop out of a filtered view.
|
|
58
|
+
// View-level cutoff only: rtk's own rows stay untouched. Timestamps that do
|
|
59
|
+
// not parse land on 0 and drop out of a filtered view.
|
|
76
60
|
const where = cutoffMs && cutoffMs > 0
|
|
77
61
|
? `WHERE CAST(strftime('%s', timestamp) AS INTEGER) >= ${Math.floor(cutoffMs / 1000)}`
|
|
78
62
|
: '';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { readPluginSettings } from './plugin-settings.js';
|
|
1
2
|
const BRIDGE_KEY = Symbol.for('tersio/combo-session-state');
|
|
3
|
+
// A verbatim sentence from OMP's subagent prompt, so it can drift; settings.json overrides it.
|
|
2
4
|
export const OMP_SUBAGENT_MARKER = 'You are operating on a piece of work assigned to you by the main agent.';
|
|
3
5
|
export const COMBO_LEVELS = Object.freeze({
|
|
4
6
|
off: Object.freeze({ level: 'off', caveman: 'off', rtk: 'off', ponytail: 'off' }),
|
|
@@ -51,12 +53,12 @@ function bridge() {
|
|
|
51
53
|
if (existing?.state)
|
|
52
54
|
return existing;
|
|
53
55
|
const initial = normalizedState((existing || COMBO_LEVELS.off));
|
|
54
|
-
return (globalThis[BRIDGE_KEY] = { state: initial, listeners: new
|
|
56
|
+
return (globalThis[BRIDGE_KEY] = { state: initial, listeners: new Map() });
|
|
55
57
|
}
|
|
56
58
|
function publish(state) {
|
|
57
59
|
const shared = bridge();
|
|
58
60
|
shared.state = state;
|
|
59
|
-
for (const listener of shared.listeners)
|
|
61
|
+
for (const listener of shared.listeners.values())
|
|
60
62
|
listener(state);
|
|
61
63
|
return state;
|
|
62
64
|
}
|
|
@@ -70,31 +72,44 @@ export function systemPromptIncludes(systemPrompt, marker) {
|
|
|
70
72
|
return asPromptArray(systemPrompt).some((prompt) => typeof prompt === 'string' && prompt.includes(marker));
|
|
71
73
|
}
|
|
72
74
|
export function isOmpSubagentPrompt(systemPrompt) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const raw = readPluginSettings().subagentMarkers;
|
|
76
|
+
const configured = Array.isArray(raw) ? raw.filter((m) => typeof m === 'string' && m.trim() !== '') : [];
|
|
77
|
+
// A malformed setting falls back rather than matching every turn.
|
|
78
|
+
const markers = configured.length ? configured : [OMP_SUBAGENT_MARKER];
|
|
79
|
+
return markers.some((marker) => systemPromptIncludes(systemPrompt, marker));
|
|
80
|
+
}
|
|
81
|
+
// ultra maps to max, lite/full to nearest, rather than erroring.
|
|
82
|
+
const COMBO_ALIASES = { ultra: 'max', lite: 'medium', full: 'balanced' };
|
|
75
83
|
export function normalizeComboLevel(value) {
|
|
76
84
|
const level = String(value || '').trim().toLowerCase();
|
|
77
|
-
|
|
85
|
+
if (isPresetLevel(level))
|
|
86
|
+
return level;
|
|
87
|
+
return COMBO_ALIASES[level] ?? null;
|
|
78
88
|
}
|
|
79
|
-
|
|
89
|
+
// OMP themes status text through ctx.ui.theme.fg; pi has no ctx.ui.theme, so
|
|
90
|
+
// the same string renders plain there.
|
|
91
|
+
export function themeStatus(ui, indicator, label, highlight = false) {
|
|
80
92
|
const theme = ui?.theme;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
if (!theme?.fg)
|
|
94
|
+
return `${indicator} ${label}`;
|
|
95
|
+
return `${highlight ? theme.fg('accent', indicator) : indicator} ${theme.fg('muted', label)}`;
|
|
96
|
+
}
|
|
97
|
+
export function paintStatusBar(ui, key, emoji, label, isActive) {
|
|
98
|
+
ui?.setStatus?.(key, themeStatus(ui, emoji, label, isActive));
|
|
99
|
+
}
|
|
100
|
+
export function statusUi(ctx) {
|
|
101
|
+
try {
|
|
102
|
+
const ui = ctx?.ui;
|
|
103
|
+
return ui?.setStatus ? ui : undefined;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Which context a status bar paints through. Never remember a UI-less event:
|
|
110
|
+
// that mutes every later bare syncStatus(), freezing the bar on a stale preset.
|
|
96
111
|
export function paintableCtx(remembered, next) {
|
|
97
|
-
return next
|
|
112
|
+
return statusUi(next) ? next : remembered;
|
|
98
113
|
}
|
|
99
114
|
// Last-wins scan for a custom session entry; skips entries whose value fails
|
|
100
115
|
// to parse so a corrupt write never shadows an older valid one.
|
|
@@ -131,8 +146,7 @@ export function setSharedComboMode(name, value) {
|
|
|
131
146
|
return getSharedComboState();
|
|
132
147
|
const modes = { ...getSharedComboState(), [name]: mode };
|
|
133
148
|
// 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.
|
|
149
|
+
// cannot strand a matching preset at 'custom' and drop the combo bar.
|
|
136
150
|
return publish(normalizedState(modes));
|
|
137
151
|
}
|
|
138
152
|
export function reconcileSharedComboEntries(entries) {
|
|
@@ -156,23 +170,17 @@ export function reconcileSharedComboEntries(entries) {
|
|
|
156
170
|
modes[name] = mode;
|
|
157
171
|
}
|
|
158
172
|
}
|
|
159
|
-
// The level
|
|
160
|
-
//
|
|
173
|
+
// The level reflects the final triplet, never the write order, so a preset
|
|
174
|
+
// entry followed by the same values reconciles back to the preset.
|
|
161
175
|
return publish(normalizedState(modes));
|
|
162
176
|
}
|
|
163
|
-
// One-line session-wide mode summary for change confirmations
|
|
164
|
-
// `caveman=ULTRA, rtk=ON, ponytail=ULTRA`.
|
|
177
|
+
// One-line session-wide mode summary for change confirmations.
|
|
165
178
|
export function activeModesSummary(state) {
|
|
166
179
|
return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
|
|
167
180
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// A null listener clears all (no current callers; reserved for teardown).
|
|
172
|
-
if (typeof listener === 'function')
|
|
173
|
-
bridge().listeners.add(listener);
|
|
174
|
-
else
|
|
175
|
-
bridge().listeners.clear();
|
|
181
|
+
// Keyed registration drops stale closures; hosts offer no teardown hook.
|
|
182
|
+
export function setSharedComboListener(key, listener) {
|
|
183
|
+
bridge().listeners.set(key, listener);
|
|
176
184
|
}
|
|
177
185
|
export function resetSharedComboState() {
|
|
178
186
|
return setSharedComboLevel('off');
|